Notes dump for Express

đź“– tl;dr: Let's learn ExpressJS


  • To serve static files such as images, CSS files, and JavaScript files, use the express.static built-in middleware function in Express.
    app.use(express.static('public'))
  • Express looks up the files relative to the static directory, so the name of the static directory is not part of the URL. Say for above:
  http://localhost:3000/images/kitten.jpg
  http://localhost:3000/css/style.css
  http://localhost:3000/js/app.js
  http://localhost:3000/images/bg.png
  http://localhost:3000/hello.html
  • The path that you provide to the express.static function is relative to the directory from where you launch your node process.
  • Synchronous functions and methods tie up the executing process until they return. A single call to a synchronous function might return in a few microseconds or milliseconds, however in high-traffic websites, these calls add up and reduce the performance of the app. Avoid their use in production.
  • Although Node and many modules provide synchronous and asynchronous versions of their functions, always use the asynchronous version in production. The only time when a synchronous function can be justified is upon initial startup.
  • console.log() or console.err() are synchronous when the destination is a terminal or a file hence not very usable in production.
  • If you’re logging app activity (for example, tracking traffic or API calls), instead of using console.log(), use a logging library like Winston or Bunyan.
  • Node uses an error-first callback convention for returning errors from asynchronous functions, where the first parameter to the callback function is the error object, followed by result data in succeeding parameters.
  • Adding an event listener for uncaughtException will change the default behavior of the process that is encountering an exception; the process will continue to run despite the exception.
  • Continuing to run the app after an uncaught exception is a dangerous practice and is not recommended
  • This is why we recommend things like multiple processes and supervisors: crashing and restarting is often the most reliable way to recover from an error.
  • The wrap() function is a wrapper that catches rejected promises and calls next() with the error as the first argument.
    const wrap = fn => (...args) => fn(...args).catch(args[2])
    
    app.get('/', wrap(async (req, res, next) => {
    const company = await getCompanyById(req.query.id)
    const stream = getLogoStreamById(company.id)
    stream.on('error', next).pipe(res)
    }))
    
  • The routing methods can have more than one callback function as arguments. With multiple callback functions, it is important to provide next as an argument to the callback function and then call next() within the body of the function to hand off control to the next callback.
  • Query strings are not part of the route path.
  • To define routes with route parameters, simply specify the route parameters in the path of the route as shown below.
    app.get('/users/:userId/books/:bookId', (req, res) => {
        res.send(req.params)
    })
  • More than one callback function can handle a route (make sure you specify the next object). For example:
    app.get('/example/b', (req, res, next) => {
    console.log('the response will be sent by the next function ...')
    next()
    }, (req, res) => {
    res.send('Hello from B!')
    })
  • If you need your middleware to be configurable, export a function which accepts an options object or other parameters, which, then returns the middleware implementation based on the input parameters.
      module.exports = function (options) {
      return function (req, res, next) {
          // Implement the middleware function based on the options object
          next()
      }
      }
    
      const mw = require('./my-middleware.js')
    
      app.use(mw({ option1: '1', option2: '2' }))
    
  • To skip the rest of the middleware functions from a router middleware stack, call next('route') to pass control to the next route.
  • Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of express.Router().
  • Error-handling middleware always takes four arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don’t need to use the next object, you must specify it to maintain the signature.
  • For errors returned from asynchronous functions invoked by route handlers and middleware, you must pass them to the next() function, where Express will catch and process them. For example:
      app.get('/', (req, res, next) => {
        fs.readFile('/file-does-not-exist', (err, data) => {
          if (err) {
            next(err) // Pass errors to Express.
          } else {
            res.send(data)
          }
        })
      })
  • If you pass anything to the next() function (except the string 'route'), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions.
  • When an error is written, the following information is added to the response:
    • The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
    • The res.statusMessage is set according to the status code.
    • The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
    • Any headers specified in an err.headers object.
  • Notice that when not calling “next” in an error-handling function, you are responsible for writing (and ending) the response. Otherwise those requests will hang and will not be eligible for garbage collection.
      function clientErrorHandler (err, req, res, next) {
        if (req.xhr) {
          res.status(500).send({ error: 'Something failed!' })
        } else {
          next(err)
        }
      }
  • A Systemd service file can be used to restart the node server everytime it crashes:
      # /etc/systemd/system/nodeserver.service
      [Unit]
      Description=Node.js Example Server
      #Requires=After=mysql.service       
      # Requires the mysql service to run first
    
      [Service]
      ExecStart=/usr/local/bin/node /opt/nodeserver/server.js
      # Required on some systems
      #WorkingDirectory=/opt/nodeserver
      Restart=always
      # Restart service after 10 seconds if node service crashes
      RestartSec=10
      # Output to syslog
      StandardOutput=syslog
      StandardError=syslog
      SyslogIdentifier=nodejs-example
      #User=<alternate user>
      #Group=<alternate group>
      Environment=NODE_ENV=production PORT=1337
    
      [Install]
      WantedBy=multi-user.target
    
  • The following illustrates programmatically what Node does on your behalf when an uncaught exception occurs:
    process.on('uncaughtException', function (er) {
      console.error(er.stack)
      process.exit(1)
    })
    You can override this behavior by adding an uncaughtException handler on the process object.
  • An uncaughtException handler should be treated as a last opportunity to say your goodbyes before calling process.exit. It is not advised to keep the process running.