NodeJS #5

Express

$ whoami

Inna Ivashchuk

Senior Software Engineer

JS developer, music fan, movie-dependent and Star Wars fan 🤓

May the Force be with you!

4+ years with GlobalLogic

6+ years in web-development

        GitHub page

Agenda

  • Routing

  • Middlewares

  • Handle requests and response

  • Error handling

  • Static serving

  • Rendering templates

  • Debugging

Express

Express is fast, unopinionated, minimalist web framework for Node.js

Web Applications

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.

APIs

With a myriad of HTTP utility methods and middleware at your disposal, creating a robust API is quick and easy.

Performance

Express provides a thin layer of fundamental web application features, without obscuring Node.js features that you know and love.

Frameworks

Many popular frameworks are based on Express (Nest.js).

Express

Middlewares

What is middleware?

     Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle.

The next middleware function is commonly denoted by a variable named next.

Middleware tasks

Middleware functions can perform the following tasks:

  • Execute any code
  • Make changes to the request and the response objects
  • End the request-response cycle
  • Call the next middleware function in the stack

Middleware types

Application-level middleware

Bind application-level middleware to an instance of the app object by using the app.use() and app.METHOD() functions, where METHOD is the HTTP method of the request that the middleware function handles (such as GET, PUT, or POST) in lowercase.

var express = require('express')
var app = express()

app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})

app.get('/user/:id', function (req, res, next) {
  // if the user ID is 0, skip to the next route
  if (req.params.id === '0') next('route')
  // otherwise pass the control to the next middleware 
  // function in this stack
  else next()
}, function (req, res, next) {
  // send a regular response
  res.send('regular')
})

Router-level middleware

      Router-level middleware works in the same way as application-level middleware, except it is bound to an instance of express.Router().

var express = require('express')
var app = express()
var router = express.Router()

// predicate the router with a check and bail out when needed
router.use(function (req, res, next) {
  if (!req.headers['x-auth']) return next('router')
  next()
})

router.get('/user/:id', function (req, res) {
  res.send('hello, user!')
})

// use the router and 401 anything falling through
app.use('/admin', router, function (req, res) {
  res.sendStatus(401)
})

Error-handling middleware

      Define error-handling middleware functions in the same way as other middleware functions, except with four arguments instead of three, specifically with the signature (err, req, res, next)):

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something goes wrong!')
})

Third-party middleware

     Use third-party middleware to add functionality to Express apps.

Install the Node.js module for the required functionality, then load it in your app at the application level or at the router level.

$ npm install cookie-parser
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')

// load the cookie-parsing middleware
app.use(cookieParser())

Routing

What is routing?

     Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on).

What is routing?

The following code is an example of a very basic route.

const express = require('express')
const app = express()

// respond with "hello world" when
// a GET request is made to the homepage
app.get('/', function (req, res) {
  res.send('hello world')
})

Route methods

A route method is derived from one of the HTTP methods, and is attached to an instance of the express class.

const express = require('express')
const app = express()

// GET method route
app.get('/cats', function (req, res) {
  res.send('GET request to the cats')
})

// POST method route
app.post('/cats', function (req, res) {
  res.send('POST request to the cats')
})

// PUT method route
app.put('/cats/:id', function (req, res) {
  res.send('PUT request to the cat by ID in cats')
})

Route methods

    Express supports methods that correspond to all HTTP request methods: get, post, and so on. For a full list, see app.METHOD.

     There is a special routing method, app.all(), used to load middleware functions at a path for all HTTP request methods.

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...')
  next() // pass control to the next handler
})

Route paths

    Route paths, in combination with a request method, define the endpoints at which requests can be made.

Route paths can be:

  • strings,
  • string patterns,
  • regular expressions.

Route paths

app.get('/', function (req, res) {
  res.send('root')
})

// this route path will match requests to /about
app.get('/about', function (req, res) {
  res.send('about')
})

// this route path will match requests to /random.txt
app.get('/random.text', function (req, res) {
  res.send('random.text')
})

// this route path will match abcd, abxcd, abRANDOMcd, ab123cd, and so on
app.get('/ab*cd', function (req, res) {
  res.send('ab*cd')
})


// this route path will match /abe and /abcde.
app.get('/ab(cd)?e', function (req, res) {
  res.send('ab(cd)?e')
})

Route paths: regular expression

// this route path will match anything with an “a” in it
app.get(/a/, function (req, res) {
  res.send('/a/')
})


// this route path will match butterfly and dragonfly,
// but not butterflyman, dragonflyman, and so on
app.get(/.*fly$/, function (req, res) {
  res.send('/.*fly$/')
})

Route parameters

     Route parameters are named URL segments that are used to capture the values specified at their position in the URL. The captured values are populated in the req.params object, with the name of the route parameter specified in the path as their respective keys.

// Route path: /users/:userId/books/:bookId
// Request URL: http://localhost:3000/users/34/books/8989
// req.params: { "userId": "34", "bookId": "8989" }

app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)
})

Route handlers

     We can provide multiple callback functions that behave like middleware to handle a request. The only exception is that these callbacks might invoke next('route') to bypass the remaining route callbacks. Can be used to impose pre-conditions on a route, then pass control to subsequent routes if there’s no reason to proceed with the current route.

// a single callback function can handle a route. For example:
app.get('/example/a', function (req, res) {
  res.send('Hello from A!')
})

// more than one callback function can handle a route 
// (make sure you specify the next object). For example:
app.get('/example/b', function (req, res, next) {
  console.log('the response will be sent by the next function ...')
  next()
}, function (req, res) {
  res.send('Hello from B!')
})

Route handlers

// an array of callback functions can handle a route. For example:
var cb0 = function (req, res, next) {
  console.log('CB0')
  next()
}

var cb1 = function (req, res, next) {
  console.log('CB1')
  next()
}

var cb2 = function (req, res) {
  res.send('Hello from C!')
}

app.get('/example/c', [cb0, cb1, cb2])

// a combination of independent functions and arrays of 
// functions can handle a route. For example:
app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('the response will be sent by the next function ...')
  next()
}, function (req, res) {
  res.send('Hello from D!')
})

app.route()

app.route('/book')
  .get(function (req, res) {
    res.send('Get a random book')
  })
  .post(function (req, res) {
    res.send('Add a book')
  })
  .put(function (req, res) {
    res.send('Update the book')
  })

We can create chainable route handlers for a route path by using app.route(). Because the path is specified at a single location, creating modular routes is helpful, as is reducing redundancy and typos. 

express.Router

var express = require('express')
var router = express.Router()

// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
  console.log('Time: ', Date.now())
  next()
})
// define the home page route
router.get('/', function (req, res) {
  res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
  res.send('About birds')
})

module.exports = routerapp.route('/book')
  .get(function (req, res) { res.send('Get a random book')  })
  .post(function (req, res) { res.send('Add a book') })
  .put(function (req, res) { res.send('Update the book') })

      Use the express.Router class to create modular, mountable route handlers. 

Create a router file named birds.js in the app directory, with the following content:

express.Router

var birds = require('./birds')

// ...

app.use('/birds', birds)

Then, load the router module in the app:

Handle requests and response

Request

The req object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.

app.get('/user/:id', function (req, res) {
  res.send('user ' + req.params.id)
})
app.get('/user/:id', function (request, response) {
  response.send('user ' + request.params.id)
})

The same behavior but full name request in use:

Request properties

GET Get a representation of the target resource’s state.
req.app Holds a reference to the instance of the Express application that is using the middleware
req.body Contains key-value pairs of data submitted in the request body
req.cookie When using cookie-parser middleware, this property is an object that contains cookies sent by the request
req.host Contains the host derived from the Host HTTP header
req.method Contains a string corresponding to the HTTP method of the request
req.params It's an object containing properties mapped to the named route “parameters”
req.path Contains the path part of the request URL
req.route Contains the currently-matched route, a string
req.secure A Boolean property that is true if a TLS connection is established

Description

Property

An example

const app = require('express')()
const bodyParser = require('body-parser')
const multer = require('multer') // v1.0.5
const upload = multer() // for parsing multipart/form-data

// for parsing application/json content-type
app.use(bodyParser.json()) 
// for parsing application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }))

app.post('/dogs?isGreat=true', upload.array(), function (req, res, next) {
  // access body (payload)
  console.log(req.body) 
  res.json(req.body)
  
  // Host: "example.com:3000"
  console.dir(req.host)
  // => 'example.com:3000'
  
  // example.com/dogs?isGreat=true
  console.dir(req.path)
  // => "/dogs"
 
  // check params
  console.log(req.params)
})

Request methods

GET Get a representation of the target resource’s state.
req.accepts() Checks if the specified content types are acceptable, based on the request’s Accept HTTP header field
req.acceptCharsets() Returns the first accepted charset of the specified character sets, based on the request’s Accept-Charset  HTTP header field
req.acceptsEncodings() Returns the first accepted encoding of the specified encodings, based on the request’s Accept-Encoding HTTP header field
req.acceptsLanguages() Returns the first accepted language of the specified languages, based on the request’s Accept-Language HTTP header field
req.get() Returns the specified HTTP request header field
req.is() Returns the matching content type if the incoming request’s “Content-Type” 
req.range() Range header parser. The size parameter is the maximum size of the resource

Description

Methods

Response

The req object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.

app.get('/user/:id', function (req, res) {
  res.send({ name: 'Luke', age: 33 })
})
app.get('/user/:id', function (request, response) {
  response.send({ name: 'Luke', age: 33 })
})

The same behavior but full name response in use:

Response properties

GET Get a representation of the target resource’s state.
res.app This property holds a reference to the instance of the Express application that is using the middleware.
res.app is identical to the req.app property in the request object.
res.headersSent Boolean property that indicates if the app sent HTTP headers for the response
res.locals An object that contains response local variables scoped to the request, and therefore available only to the view(s) rendered during that request/response cycle (if any). Otherwise, this property is identical to app.locals

Description

Methods

Response methods

res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>'])
res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly')
res.append('Warning', '199 Miscellaneous warning')

// cookie
res.cookie('name', 'tobi',
    { domain: '.example.com', path: '/admin', secure: true })
res.cookie('rememberme', '1',
    { expires: new Date(Date.now() + 900000), httpOnly: true })

Response methods

GET Get a representation of the target resource’s state.
res.download() Prompt a file to be downloaded
res.end() End the response process
res.json() Send a JSON response
res.jsonp() Send a JSON response with JSONP support
res.redirect() Redirect a request
res.render() Render a view template
res.send() Send a response of various types
res.sendFile() Send a file as an octet stream
res.sendStatus() Set the response status code and send its string representation as the response body

Description

Methods

Response properties

app.get('/', function (req, res) {
  console.log(res.headersSent) // false
  res.send('OK')
  console.log(res.headersSent) // true
})

// locals
app.use(function (req, res, next) {
  res.locals.user = req.user
  res.locals.authenticated = !req.user.anonymous
  next()
})

Error handling

Catching Errors

   It’s important to ensure that Express catches all errors that occur while running route handlers and middleware.

    If synchronous code (routers) throws an error, then Express will catch and process it. For example:

app.get('/', function (req, res) {
  throw new Error('BROKEN') // Express will catch this on its own.
})

Catching Errors: Async code

Async errors requires extra work:

app.get('/', function (req, res, next) {
  fs.readFile('/file-does-not-exist', function (err, data) {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

// we could also use a chain of handlers to rely on synchronous error catching, 
// by reducing the asynchronous code to something trivial. For example:
app.get('/', [
  function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', function (err, data) {
      res.locals.data = data
      next(err)
    })
  },
  function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
  }
])

The default error handler

     The default error-handling middleware function is added at the end of the middleware function stack.

     If we pass an error to next() - it will be handled by the built-in error handler; the error will be written to the client with the stack trace.

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

Writing error handlers

     Define error-handling middleware functions in the same way as other middleware functions, except error-handling functions, which have four arguments instead of three: (err, req, res, next). For example:

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

Static serving

Serving static files in Express

    To serve static files such as images, CSS files, and JavaScript files, use the express.static built-in middleware function in Express.

The function signature is:

// The root argument specifies the root directory 
// from which to serve static assets. 
// For more information on the options argument,
// see express.static.
 
express.static(root, [options])
app.use(express.static('public'))
app.use(express.static('files'))

Serving static files in Express

And here is a common way to serve some static file:

const path = require('path')
app.use('/static', express.static(path.join(__dirname, 'public')))

Rendering templates

Using template engines with Express

A template engine enables to use static template files in our application. At runtime, the template engine replaces variables in a template file with actual values and transforms the template into an HTML file sent to the client. 

Some popular template engines that work with Express are Pug, Mustache, and EJS. The Express application generator uses Jade as its default, but it also supports several others.

Template engines + React

$ npm install express-react-views react react-dom

express-react-views is an Express view engine that renders React components on the server. It renders static markup and does not support mounting those views on the client.

// app.js

var app = express();

app.set('views', __dirname + '/views');
app.set('view engine', 'jsx');
app.engine('jsx', require('express-react-views').createEngine());

Template engines + React

      Views should be node modules that export a React component. Let's assume you have this file in views/index.jsx:

var React = require('react');

function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}

module.exports = HelloMessage;

Template engines + React

      Routes would look identical to the default routes Express gives you out of the box.

// app.js
app.get('/', require('./routes').index);

// routes/index.js
exports.index = function(req, res){
  res.render('index', { name: 'John' });
};

Template engines + React

     Simply pass the relevant props to a layout component.

// views/layouts/default.jsx:
var React = require('react');

function DefaultLayout(props) {
  return (
    <html>
      <head><title>{props.title}</title></head>
      <body>{props.children}</body>
    </html>
  );
}

module.exports = DefaultLayout;

// views/index.jsx:
var React = require('react');
var DefaultLayout = require('./layouts/default');

function HelloMessage(props) {
  return (
    <DefaultLayout title={props.title}>
      <div>Hello {props.name}</div>
    </DefaultLayout>
  );
}

module.exports = HelloMessage;

SSR

Server-side rendering (SSR) is a popular technique for rendering a client-side single-page application (SPA) on the server. 

This approach can be useful for:

  • search engine optimization (SEO) when indexing
  • in situations where downloading a large JavaScript, bundle is impaired by a slow network

We can use NPM package - @react-ssr/express

Debugging

Let's debug

For example, we have following code:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
    res.send('How to Debug Node Express App !')
});

app.listen(port, =
   () => console.log(`Example app listening on port ${port}!`))

Debug Node Express project from VS Code

Step 1: Check configuration of Node.js debug (launch.json)

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "Attach to NodeJS",
            "port": 9229,
            "restart": true,
            "stopOnEntry": false,
            "protocol": "inspector"
        }
    ]
}

Debug Node Express project from VS Code

Step 2: go to the Debug option in VS Code and click on the Start Debugging option.

Debug Node Express project from VS Code

Step 3: Run Node app like that

 node --inspect server.js

Debug Node Express project from VS Code

Step 4: Now the application will be running at http://localhost:3000 in debug mode. Add a breakpoint inside route in our app.js code:

When the / route gets hit the break point will come into action and we can debug our code.

Debug Node Express using debug module

       Express uses the debug module internally to log information about route matches, middleware functions that are in use, application mode, and the flow of the request-response cycle.

    To see all the internal logs used in Express, set the DEBUG environment variable to express:* when launching your app.

$ DEBUG=express:* node index.js


# on Windows, use the corresponding command
> set DEBUG=express:* & node index.js

Debug Node Express using debug module

Running this command on the default app generated by the express generator prints the following output:

$ DEBUG=express:* node ./bin/www
  express:router:route new / +0ms
  express:router:layer new / +1ms
  express:router:route get / +1ms
  express:router:layer new / +0ms
  express:router:route new / +1ms
  express:router:layer new / +0ms
  express:router:route get / +0ms
  express:router:layer new / +0ms
  express:application compile etag weak +1ms
  express:application compile query parser extended +0ms
  express:application compile trust proxy false +0ms
  express:application booting in development mode +1ms
  express:router use / query +0ms
  express:router:layer new / +0ms
  express:router use / expressInit +0ms
  express:router:layer new / +0ms
  express:router use / favicon +1ms
  express:router:layer new / +0ms
  express:router use / logger +0ms
  express:router:layer new / +0ms
  express:router use / jsonParser +0ms
  express:router:layer new / +1ms
  express:router use / urlencodedParser +0ms
  express:router:layer new / +0ms
  express:router use / cookieParser +0ms
  express:router:layer new / +0ms
  express:router use / stylus +90ms
  express:router:layer new / +0ms
  express:router use / serveStatic +0ms
  express:router:layer new / +0ms
  express:router use / router +0ms
  express:router:layer new / +1ms
  express:router use /users router +0ms
  express:router:layer new /users +0ms
  express:router use / &lt;anonymous&gt; +0ms
  express:router:layer new / +0ms
  express:router use / &lt;anonymous&gt; +0ms
  express:router:layer new / +0ms
  express:router use / &lt;anonymous&gt; +0ms
  express:router:layer new / +0ms

Q & A

Homework

  1. Create a new directory tutorial5

  2. Create an app with the next routes: cats, dogs, birds

  3. Using express-react-views render static data (arrays with info) and images (external URL can be used) per route

  4. Add routes to be able to fetch data by ID per animal - "/api/cats/:id"

  5. Add error handlers

  6. Try to debug your code using VS Code

Helpful souces

NodeJS Core #5

By Inna Ivashchuk

NodeJS Core #5

  • 570