# Express Best Practices Learn best development practices for [Express][express] web applications. **You will need** * A working [Express][express] application **Recommended reading** * [Express](../express/) * [Mongoose](../mongoose/) --- ## Use environment variables for configuration .breadcrumbs[
Express Best Practices
] Never hardcode configuration into your application, as it makes it difficult to deploy in different environments. You may also uninentionally expose sensitive data such as secret keys. [Environment variables][node-process-env] are a suitable alternative. There is already an example in a freshly generated Express application in the `bin/start.js` file: ```js const port = process.env.PORT || 3000; ``` That line is equivalent to the following code: ```js let port; if (process.env.PORT) { port = process.env.PORT; } else { port = 3000; } ``` --- ### Running your application with environment variables .breadcrumbs[
Express Best Practices
>
Use environment variables for configuration
] Assuming this is the command to run your application: ```bash $> npm start ``` In a Bash shell, you can prefix it with any environment variable(s) you may want to set: ```bash $> PORT=4000 FOO=bar npm start ``` You can also set them with `export` for the remainder of your Bash session, before running your application: ```bash $> export PORT=4000 $> export FOO=bar $> npm start ``` > On a server or in a cloud environment, you want to edit the process manager's > or cloud platform's configuration for your application. For example, on > [Render][render], you can configure environment variables in your application's > settings web page. --- ### Create a configuration file if you have many variables .breadcrumbs[
Express Best Practices
>
Use environment variables for configuration
] If you use many environment variables for configuration, you may want to centralize your configuration code in a single file, for example `config.js`: ```js // File: config.js export const port = process.env.PORT || 3000; export const secretKey = process.env.MY_APP_SECRET_KEY || 'changeme'; ``` This avoids repetition if you use the same variable in different places, and serves as a sort of documentation of all your configuration parameters and their default values. You can simply require this file and use its variables where needed: ```js // File: some other file import * as config from '../path/to/config.js'; server.listen(config.port); ``` --- ### Validate complex configuration variables .breadcrumbs[
Express Best Practices
>
Use environment variables for configuration
] You always get a string value (or undefined) when you retrieve an environment variable. No check is performed for you. Some of your configuration parameters may be mandatory. Some probably need to be a specific kind of value like a positive integer or an URL. If you have a centralized configuration file like suggested earlier, you can simply add some **validation** code and throw errors in case the values are not as expected: ```js // Validate that port is a positive integer. if (process.env.PORT) { const parsedPort = parseInt(process.env.PORT, 10); if (!Number.isInteger(parsedPort)) { throw new Error('Environment variable $PORT must be an integer'); } else if (parsedPort < 1 || parsedPort > 65535) { throw new Error('Environment variable $PORT must be a valid port number'); } } // Validate that some environment variable is set. if (!process.env.MY_APP_FOO) { throw new Error('Environment variable $MY_APP_FOO must be set'); } ``` --- ### The `dotenv` package .breadcrumbs[
Express Best Practices
>
Use environment variables for configuration
] If you use many environment variables for configuration, it can be a pain to set them all when starting your application for local development. [`dotenv`][dotenv] is a popular npm package that can **auto-fill your project's environment variables from a configuration file** named `.env` with the following format: ``` PORT=4000 MY_APP_SECRET=letmein ``` To use it, the first thing you should do it **ignore this `.env` file**, as you don't want to unintentionally commit sensitive information into your repository: ```bash $> echo .env >> .gitignore $> git add .gitignore $> git commit -m "Ignore .env file" ``` > You can share this `.env` file among your team members, and everyone can adapt > it to their local environment if necessary. But never commit it. --- #### Installing and using `dotenv` .breadcrumbs[
Express Best Practices
>
Use environment variables for configuration
>
The `dotenv` package
] Install `dotenv` as a development dependency: ```bash npm install --save-dev dotenv ``` Then add the following code to the top of your configuration file (or wherever you retrieve configuration from environment variables): ```js // Load environment variables from the .env file. * import * as dotenv from 'dotenv' * dotenv.config() // Retrieve configuration from environment variables. const port = process.env.PORT || 3000; // ... ``` > Make sure that you execute the `dotenv.config()` line **before > accessing any environment variable in `process.env`**, otherwise it will be > too late. You'll be fine if you use a centralized configuration file and put > that code at the top. --- ## The `debug` package .breadcrumbs[
Express Best Practices
] The [`debug` package][debug] is a popular tool for debugging in Node.js applications, which you may use instead of `console.log`. It is included in most generated Express applications by default. The idea is that you create a named debug logger which you can then use to log debug messages as things happen in your application: ```js import debug from 'debug'; const log = debug('app:movies'); log('Creating movie'); log('Successfully created movie'); log('Something happened'); ``` These are **debug logs**, meaning that they **are not displayed by default**. They are meant to be enabled when you want to debug the behavior of your application in more details. --- ### Enabling debug logs .breadcrumbs[
Express Best Practices
>
The `debug` package
] The `debug` package decides whether to actually display message depending on the value of the `$DEBUG` environment variable. You can enable all debug logs by setting it to `*`: ```bash $> DEBUG=* npm start ``` Keep in mind that `debug` is a popular package and is not specific to Express. Other packages in your dependency tree might be using it (e.g. Express does). To only display a subset of the logs, you can specify a prefix: ```bash $> DEBUG=app:* npm start ``` > This would display the logs from all debug loggers that have a name starting > with `app:`. You may use this to differentiate logs within your application, > e.g. `app:database`, `app:http`, `app:api`, etc. --- ### More powerful logging .breadcrumbs[
Express Best Practices
>
The `debug` package
] The `debug` package is a minimalistic logging solution. For more features, use a more advanced library such as: * [bunyan] * [log4js] * [nightingale] * [winston] --- ## Use routers .breadcrumbs[
Express Best Practices
] **Do NOT** define all your routes in `app.js`; it will get **too large and hard to maintain**. Group your API routes **by feature** and create a router for each group in the `routes` directory, then `import` them in `app.js`: ```js import express from 'express'; import `peopleApiRouter` from './routes/people.js'; import `moviesApiRouter` from './routes/movies.js'; const app = express(); // Basic middlewares configuration here (e.g. bodyParser, static)... app.use('/api/people', `peopleApiRouter`); app.use('/api/movies', `moviesApiRouter`); ``` --- ## Avoid repetition with middleware .breadcrumbs[
Express Best Practices
] You often end up with **code duplication in routes**: ```js router.get('/:id', function(req, res, next) { * Person.findById(req.params.id).exec().then(person => { * if (!person) return res.sendStatus(404); // Send user here * }) * .catch(err => next(err)); }); router.patch('/:id', function(req, res, next) { * Person.findById(req.params.id).exec().then(person => { * if (!person) return res.sendStatus(404); // Update and send user here * }) * .catch(err => next(err)); }); router.delete('/:id', function(req, res, next) { * Person.findById(req.params.id).exec().then(person => { * if (!person) return res.sendStatus(404); // Delete user here * }) * .catch(err => next(err)); }); ``` --- ### Writing middleware functions for common tasks .breadcrumbs[
Express Best Practices
>
Avoid repetition with middleware
] You can write a **middleware function** that performs only this task and **attaches the Person document to the `req` object**: ```js function loadPersonFromParams(req, res, next) { Person.findById(req.params.id) .exec() .then(person => { if (!person) { return res.status(404).send(\`No person found with ID ${req.params.id}`); } req.person = person; next(); }) .catch(err => next(err)); } ``` --- ### Plugging your middleware function into routes .breadcrumbs[
Express Best Practices
>
Avoid repetition with middleware
] You can plug this function into the routes that need it. Your handler functions can then simply use `req.person`, as it will have been **loaded before they are executed**: ```js router.get('/:id', `loadPersonFromParams`, function (req, res, next) { res.send(`req.person`); }); router.patch('/:id', `loadPersonFromParams`, function (req, res, next) { // Update req.person here `req.person` .save() .then((updatedPerson) => { res.send(updatedPerson); }) .catch((err) => next(err)); }); router.delete("/:id", `loadPersonFromParams`, function (req, res, next) { `req.person` .deleteOne() .then(res.sendStatus(204)) .catch((err) => next(err)); }); ``` --- ## Handling Promises .breadcrumbs[
Express Best Practices
] So far, all of our examples have used chainable methods (`.then()` and `.catch()`) to handle Promises returned by Mongoose. ```js router.get('/person/:id', (req, res, next) => { Person.findById(req.params.id) * .then(person => res.json(user)) * .catch(err => next(err)); }); ``` This is absolutely fine, but **you might be more comfortable using `async/await`**. --- ### Handling Promises using `async/await` .breadcrumbs[
Express Best Practices
>
Handling Promises
] In that case, you may write your Express functions like this: ```js router.get('/person/:id', `async` (req, res, next) => { try { const user = `await` Person.findById(req.params.id); res.json(user); } catch (err) { next(err); } }); ``` However, you will need to wrap every callback in those **annoying `try/catch` blocks** in order to handle errors. This leads to a lot of unnecesary repetition. **Could we do better?** --- ### Async Handler wrapper function .breadcrumbs[
Express Best Practices
>
Handling Promises
] We can create a **wrapper function** that automatically catches errors and passes them to the next Express middleware. ```js const asyncHandler = fn => (req, res, next) => Promise .resolve(fn(req, res, next)) .catch(next); ``` `asyncHandler` is a **higher-order function** that takes an async function (`fn`) and **returns a new function**. The returned function, when called, runs `fn` and catches any unhandled errors, **passing them to next()** (i.e., the next middleware). --- ### Using `asyncHandler` wrapper function .breadcrumbs[
Express Best Practices
>
Handling Promises
] You can now use this wrapper as middleware in your Express chain. Errors will be automatically caught. ```js router.get('/user/:id', `asyncHandler`(`async` (req, res) => { const user = await User.findById(req.params.id); res.json(user); })); ``` --- ## Error Handling .breadcrumbs[
Express Best Practices
] The code generated by the express-api-es-generator deals with error handling in a very basic manner. ```js app.use(function (`err`, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // Send the error status res.status(err.status || 500); res.send(err.message); }); ``` This **middleware function** is specifically designed to handle errors. We can know this because the callback takes **four arguments, the first of which being the `err` object.** When an error is passed to the `next()` function or thrown in a synchronous function, it will be caught by this error-handling middleware. This works fine for Express errors, but **we also need to handle errors thrown by MongoDB.** --- ### MongoDB errors .breadcrumbs[
Express Best Practices
>
Error Handling
] Imagine the following scenario: one of your Mongoose models specifies that a `Person` must have a unique email. Someone using your API make a `POST` request to `/person`, trying to create a new person with an email that has already been registered. If this happens, an **ugly error message and generic status code will be sent to the user**: ```txt E11000 duplicate key error collection ``` --- ### Handling specific errors .breadcrumbs[
Express Best Practices
>
Error Handling
] In order to get a **meaningful status code and prettier error message**, we could adjust the error handling middleware to **check for this specific case**: ```js app.use(function (`err`, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; * if (err.code === 11000) { * res.status(409).send('Email already registered.'); * } else { // Send the error status res.status(err.status || 500); res.send(err.message); } }); ``` [bunyan]: https://github.com/trentm/node-bunyan [debug]: https://www.npmjs.com/package/debug [dotenv]: https://www.npmjs.com/package/dotenv [express]: https://expressjs.com [render]: https://render.com/docs/configure-environment-variables#configuring-secrets-and-other-environment-information-on-render [log4js]: https://www.npmjs.com/package/log4js [mongoose]: http://mongoosejs.com [nightingale]: https://www.npmjs.com/package/nightingale [node-process-env]: https://nodejs.org/docs/latest-v12.x/api/process.html#process_process_env [winston]: https://www.npmjs.com/package/winston