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!
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.
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
An Express application can use the following types of middleware:
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
// 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 / <anonymous> +0ms
express:router:layer new / +0ms
express:router use / <anonymous> +0ms
express:router:layer new / +0ms
express:router use / <anonymous> +0ms
express:router:layer new / +0ms
Q & A
Homework
-
Create a new directory tutorial5
-
Create an app with the next routes: cats, dogs, birds
-
Using express-react-views render static data (arrays with info) and images (external URL can be used) per route
-
Add routes to be able to fetch data by ID per animal - "/api/cats/:id"
-
Add error handlers
-
Try to debug your code using VS Code
Helpful souces
- My repository with NodeJS tutorials
- official Express documentation
- and of course Stackoverflow
NodeJS Core #5
By Inna Ivashchuk
NodeJS Core #5
- 564