This content originally appeared on DEV Community and was authored by Jon Webb
- Previous post: How to start a Node project from scratch (with Typescript)
- Source code: Github
The baseline
project
This post assumes you are following along with the baseline
project tutorial, but the concepts apply to any Typescript project.
Node best practices
Logging
We are going to use a logging library called pino
for our application. Using a mature logging library is recommended because it allows us to access structured log data as JSON objects. Later, we can configure it to persist log data by outputting logs to a file or an external server.
Install pino
, along with its type definitions:
$ yarn add pino pino-pretty && yarn add -D @types/pino
Create a util
folder in your project source directory and create the logger.ts
file:
$ mkdir src/util && touch src/util/logger.ts
Create and export a pino
logger instance. For now, we will use the default transport, which logs to the console. We will also enable prettyPrint
when we are not in a production environment:
// src/util/logger.ts
import pino from "pino";
export const logger = pino({
level: "info",
prettyPrint: process.env.NODE_ENV !== "production",
});
Error handling
When your application encounters an unknown error, it should terminate. From the official Node.js documentation:
By the very nature of how
throw
works in JavaScript, there is almost never any way to safely "pick up where it left off", without leaking references, or creating some other sort of undefined brittle state.The safest way to respond to a thrown error is to shut down the process. Of course, in a normal web server, there may be many open connections, and it is not reasonable to abruptly shut those down because an error was triggered by someone else.
The better approach is to send an error response to the request that triggered the error, while letting the others finish in their normal time, and stop listening for new requests in that worker.
Let's ensure that our application catches any unsafe errors by passing them to a centralized error handler.
$ touch src/util/error.ts
To ensure that our application issues a final log message when we crash it,
pino
provides a final
function that we will use to issue a fatal log:
// src/util/error.ts
import pino from "pino";
import { logger } from "./logger";
export const handle = pino.final(logger, (err, finalLogger) => {
finalLogger.fatal(err);
process.exitCode = 1;
process.kill(process.pid, "SIGTERM");
});
Now, in our main file src/index.ts
, let's ensure that any unhandled errors are passed to our handle
function. To do that, we will add listeners to the unhandledRejection
and uncaughtException
events in the Node process:
// src/index.ts
import { handle } from "./util/error";
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
handle(err);
});
Our application will now issue a final log before terminating on any unhandled errors.
express
app setup
(express
)[https://expressjs.com] is an extremely popular web framework for node.js
, and it's what we will use for the baseline
project.
In your project folder, install express
, along with its type definitions:
$ yarn add express && yarn add -D @types/express
We are also going to use pino-http
, which is a middleware that logs https requests to your server using the pino
logger:
$ yarn add pino-http && yarn add -D @types/pino-http
I prefer to store the definition of my express
application separately from the code that starts the HTTP server.
Create a file called app.ts
in your source folder:
$ touch src/app.ts
In app.ts
, we will set up a basic express
application, register built-in middlewares for parsing JSON request bodies and encoded URLs, register the pino-http
middleware, and create a single healthcheck route that we can use to check the status of our server. Finally, we will export the express
app so that we can use it elsewhere:
// src/app.ts
import express from "express";
import pinoHttp from "pino-http";
import { logger } from "./util/logger";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(pinoHttp({ logger }));
app.get("/api/health", (req, res) => res.send({ message: "OK" }));
export { app };
Server config
Our server configuration may change depending on the environment we are running it in. We may also need to use sensitive values (like database authentication credentials) that should not be exposed in our source code. For these reasons, we will store our application configuration in the environment using environmental variables managed by the operating system.
For development, it's convenient to use a file named .env
, which is not checked in to version control, to manage those variables on the fly. We will use a library called dotenv
to parse .env
files and set the corresponding environmental variables:
$ yarn add dotenv
It is important that our .env
files do not get checked into source control, since they may contain sensitive information. Let's update our .gitignore
file to ensure they are excluded:
#.gitignore
node_modules
dist
.env
Now create a .env
file in the project root, and populate it with a single PORT
variable:
$ touch .env
# .env
PORT=5000
Next, let's load any .env
variables as the first thing we do when we run src/index.ts
:
// src/index.ts
import { config } from "dotenv";
config();
// error listeners ...
The config
function exported by dotenv
parses our .env
file and sets the environmental variables accordingly so that we can use them throughout our application.
Starting and stopping the server
When our application terminates, due to external input or an internal error, there may be a number of ongoing client connections that are in the process of being resolved. Rather than abruptly terminating those connections, we want to allow any existing connections to resolve before shutting down the server gracefully.
To do this, we need to store a list of ongoing connections and implement logic to ensure connections are closed before the process is allowed to end. Rather than implement that logic ourselves, we are going to use a library called http-terminator
that does it for us:
$ yarn install http-terminator
Now, in src/index.ts
, we will start the server and use http-terminator
to gracefully close the server if a shutdown signal is received:
// src/index.ts
import { createHttpTerminator } from "http-terminator";
import { app } from "./app";
// existing code ...
const server = app.listen(process.env.PORT || 3000, () => {
logger.info(
`started server on :${process.env.PORT || 3000} in ${
process.env.NODE_ENV
} mode`
);
});
const httpTerminator = createHttpTerminator({ server });
const shutdownSignals = ["SIGTERM", "SIGINT"];
shutdownSignals.forEach((signal) =>
process.on(signal, async () => {
logger.info(`${signal} received, closing gracefully ...`);
await httpTerminator.terminate();
})
);
Notice that we are using process.env.PORT
to set the port that express
binds to, which should be loaded from our .env
file. Otherwise, we use port 3000
as a fallback.
We are also registering listeners on the SIGINT
and SIGTERM
events, which are issued when node
receives a signal from the environment to terminate the process. Earlier, when we implemented our error handler function, we told node
to issue a SIGTERM
event when terminating the process. This means our graceful shutdown listener will be called when closing the process from our error handling code, or when the process terminates from an external signal.
Your final src/index.ts
should look like this:
// src/index.ts
import { config } from "dotenv";
import { createHttpTerminator } from "http-terminator";
import { app } from "./app";
import { handle } from "./util/error";
import { logger } from "./util/logger";
config();
process.on("unhandledRejection", (err) => {
throw err;
});
process.on("uncaughtException", (err) => {
handle(err);
});
const server = app.listen(process.env.PORT || 3000, () => {
logger.info(
`started server on :${process.env.PORT || 3000} in ${
process.env.NODE_ENV
} mode`
);
});
const httpTerminator = createHttpTerminator({ server });
const shutdownSignals = ["SIGTERM", "SIGINT"];
shutdownSignals.forEach((signal) =>
process.on(signal, async () => {
logger.info(`${signal} received, closing gracefully ...`);
await httpTerminator.terminate();
})
);
Testing the server
Let's start the server using the yarn
scripts we set up in the last post:
$ yarn dev
You should see a log message that includes the port we set in our .env
file earlier:
[1621625365575] INFO (90294 on Jons-MacBook-Pro.local): started server on :5000 in development mode
Now, using your browser, or an API testing tool like Postman, make a GET
request to the healthcheck route we implemented earlier:
GET http://localhost:5000/api/health
The response should be:
{
"message": "OK"
}
Now try terminating your application from the terminal by pressing ctrl-C
, which sends a SIGINT
signal to the node
process. You should see a log message showing that our graceful termination code is being executed:
[1621626736712] INFO (93255 on Jons-MacBook-Pro.local): SIGINT received, closing gracefully ..
Commit
Go ahead and stage your changes:
$ git add .
And commit them to source control:
$ git commit
This content originally appeared on DEV Community and was authored by Jon Webb
Jon Webb | Sciencx (2021-05-21T19:58:50+00:00) How to properly create an Express server (with Typescript). Retrieved from https://www.scien.cx/2021/05/21/how-to-properly-create-an-express-server-with-typescript/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.