API with Node.js, Express and Prisma

Express

What is Express?

Express is a highly popular library in the Node.js community. In fact, Node.js itself adopted Express within its ecosystem. However, this doesn’t mean it’s the only one or even the best one. While many other libraries and frameworks exist to create REST APIs, Express is both simple and powerful. As stated on the Express website: “With a myriad of HTTP utility methods and middleware at your disposal, creating a robust API is quick and easy.”

Setting up Express

The first thing you’ll need to do is install the express module as a dependency of the application:

npm install express

This will add a new entry in the package.json file under the dependencies section, indicating the installed version.

Next, create a new file named index.js inside the app directory (where the application will reside):

touch app/index.js

Similarly to the previous section, start by taking the Hello World example from the Express documentation and replace the contents of the file with the following code:

// app/index.js

import express from 'express';

const port = 3000;

const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

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

This is very similar to the web server created earlier in the project root, but with some subtle differences:

  • The express library is used instead of the http module.
  • An express application is created (you can create multiple) and stored in the app variable.
  • This time, only requests to the root of the API (/) are accepted, and only with the GET method.

In the terminal where the previous web server is running, stop it with the CTRL+C key combination. Test this new server using the command node app/index, then access the following address http://localhost:3000/ in your web browser.

At first glance, you’ll see the same result, but express has provided a bit more simplicity in the code and has incorporated a lot of functionality, which will be explored in the upcoming sections.

More information:

Modularize the application

Before proceeding, it’s important to organize the application by creating different modules, each with its specific purpose. This separation allows us to distribute the responsibilities of each module, organize them into directories to group related functionalities, and keep the index.js file lighter, more readable, and structured.

D>For this type of application, it’s not a good practice to place all the code in a single file, which would be difficult to maintain. This is referred to as a monolithic application.

Modify the Express application as follows:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

Now, the module has a single responsibility, which is to generate and export the application. The exported value will be named app, making it independent of the library used. When imported, it will be referred to as app rather than express.

Back in the main file, rewrite it by removing the multiple responsibilities it had, leaving it with the sole responsibility of initializing each component of the application using the previously created modules:

// index.js

import http from 'http';

import { configuration } from './app/config.js';
import { app } from './app/index.js';

const { port } = configuration.server;

const server = http.createServer(app);

server.listen(port, () => {
  console.log(`Server running at port: ${port}`);
});

The createServer function from the http library is compatible with the version of the application server (app). This is very convenient, so if in the future an https server were to be added, you’d only need to include the library and duplicate the server creation line, using the same application (app).

Every time there’s a change in the configuration, it’s necessary to restart the server. Press CTRL+C in the terminal and start again in development mode using npm run dev.

If you access the address http://localhost:3000 in your web browser, everything should be working perfectly.

Express Middleware

Middleware functions are the most important part of the Express library. Here’s their definition:

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 (next).

(Taken from Using Middleware)

To understand this better, look at the following code snippet from the app/index.js file:

app.get('/', (req, res, next) => {
  res.send('Hello World');
});

The get function is the HTTP verb defined for this endpoint and has two parameters:

  1. The declared route, which in this case is the root of the API (/).
  2. The callback function that will be called when the server receives a request that matches the root route and the GET verb in the example above.

This callback function is a middleware that has access to the req (request) object for the incoming request and the res (response) object to send a response to the user. next is another function that, when invoked, allows the flow to continue so that other middleware functions can proceed with processing the request, if necessary.

Thus, each middleware specifies the request it will process based on its declaration and parameters.

What would happen at this point if the user makes a request different from those declared in the endpoints? The Express library would return a 404 status code. However, it allows the programmer to take control by adding another middleware at the end of the endpoint declarations to capture requests that were not processed by any middleware defined earlier:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res, next) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  res.status(404);
  res.json({
    message: 'Error. Route not found',
  });
});

Note several important things in the previous code snippet:

  1. Unlike the previous endpoint, this one has no route, which tells Express that if it reaches this point, it’s because no response was sent to the user in the previously declared middlewares.
  2. The next function has been added to the root route middleware, but it’s not actually used within the function. This is done to match the signature of middleware functions.
  3. The application (app) is instructed to use a middleware function. Again, it’s recognized as an Express middleware function by its signature and parameter structure.
  4. The HTTP response code is set using res.status(404), which corresponds to a “Not Found” page, indicating that a route or resource was not found.
  5. When a web server responds, it needs to indicate the type of content it’s sending to the client, so the client can interpret and display it correctly. This is done through the Content-type header in the response. By default, the value is text/plain for plain text or text/html for HTML code. These values are known as the MIME Types of files. In this case, a JSON object is explicitly responded to using the res.json() function. There’s no need to set the Content-Type header explicitly, as Express does this automatically and sets it to application/json. Additionally, it converts the JavaScript object literal to a JSON object.

When you save the file, the web server will restart due to the --watch option. Accessing a non-existent route like http://localhost:3000/unknown will result in the message defined in the middleware. This middleware becomes the last middleware function out of all those declared previously in this Express application:

{
  message: 'Error. Route not found';
}

To provide an analogy for the definition of middlewares, it’s like a queue of functions in which the request is checked starting from the first function. If a middleware matches the request, it processes the request and can do two things: respond to the request, in which case the review process stops entirely, or pass the request to the next middleware using the next function. Otherwise, if none of the middlewares match, the request will reach the end, which is the generic middleware that captures all requests that couldn’t be processed earlier.

Error Handling

Now the question arises: How can errors or exceptions that occur within middlewares be controlled? Express allows you to define an error handling middleware function with a slightly different parameter signature than the previous ones. Add it to the end of the file:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  res.status(404);
  res.json({
    message: 'Error. Route not found',
  });
});

// Error handler
app.use((err, req, res, next) => {
  const { status = 500, message } = err;

  res.status(status);
  res.json({
    error: {
      status,
      message,
    },
  });
});

To review this new Error middleware:

  1. The error-handling middleware in express starts with an error parameter abbreviated as err, following the convention of callbacks in Node.js.
  2. Retrieves the status from the err object. If it’s not provided, it sets it to 500, which by default indicates an Internal Server Error. It also retrieves the message from the error object.
  3. Finally, it sets the response status using res.status and sends back a JSON object with the error message.

Whenever an error is thrown within any middleware, this error-handling middleware will capture and process it. This can be used, for example, to save relevant error information in logs.

Now that you have an error-handling middleware, you can also invoke it from other middlewares. This is one of the purposes of the next function: to call the error-handling middleware:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

// No route found handler
app.use((req, res, next) => {
  next({
    status: 404,
    message: 'Error. Route not found',
  });
});

...

The three dots (…) are a convention to indicate the rest of the file’s content.

Now, if you invoke the next function with an argument, it will jump directly to the next error-handling middleware. This centralizes error and exception handling in a single middleware.

Finally, change the definition of the root route again so that all middlewares return JSON format:

// app/index.js

import express from 'express';

export const app = express();

app.get('/', (req, res) => {
  res.json({
    message: 'Welcome to the API',
  });
});

...