Starting from this chapter?

Clone the application repo and check out the creating-data-models-and-service branch:

git clone git@github.com:auth0-blog/wab-ts-express-api.git \
express-ts-api \
--branch creating-data-models-and-service

Make the project folder your current directory:

cd express-ts-api

Install the project dependencies:

npm i

Create a .env hidden file:

touch .env

Populate .env with this:

PORT=7000

Execute the following command to run the webpack bundler:

npm run webpack

Finally, run the server:

npm start

For this application, you'll create endpoints to access an items resource to perform read and write operations on menu items:

# get all items
GET items/
# get a single item using an id parameter
GET items/:id
# create an item
POST items/
# update an item
PUT items/
# delete an item using an id parameter
DELETE items/:id

Only the GET endpoints are public, so let's start with those.

Create Express Controllers Using TypeScript

Instead of defining the application routes inside the entry-point file, index.ts, you can create an Express router as a separate module with all your route handling details and import it wherever it's needed.

To start, under the /src/items directory, create an items.router.ts file:

touch src/items/items.router.ts

When a client application makes a request to your server, Express forwards the request to functions designed to handle that type of request (GET or POST) on the specified resource (items/). As such, each of these functions defines a route handler, which is also commonly referred to as a controller.

Populate src/items/items.router.ts with the following template that outlines the architecture of the router:

/**
 * Required External Modules and Interfaces
 */

/**
 * Router Definition
 */

/**
 * Controller Definitions
 */

// GET items/

// GET items/:id

// POST items/

// PUT items/

// DELETE items/:id

With the template in place, add the following under the Required External Modules and Interfaces section:

/**
 * Required External Modules and Interfaces
 */

import express, { Request, Response } from "express";
import * as ItemService from "./items.service";
import { Item } from "./item.interface";
import { Items } from "./items.interface";

Here, you are importing the express package and two of its internal type definitions, Request and Response, which you can use to type the callback functions of your Express controllers.

You also import all the exported functions from the items.service module and bundle them locally as an ItemService object, which makes it easier for you to avoid name collisions and to pinpoint from what package a function comes.

Finally, you also import the Item and Items interfaces, which are necessary to type the return values from the ItemService functions.

Next, define an Express router under the Router Definition section:

/**
 * Router Definition
 */

export const itemsRouter = express.Router();

Here, you use the express.Router class to create a modular and mountable bundle of route handlers. An Express router instance is often referred to as a "mini-app" because it functions as a complete middleware and routing system, which is essential for organizing the architecture of your Node.js project into components that can be easily tested and re-used.

You export the itemsRouter right away, even though its routing properties have not yet been defined. Any property that you define later in the module on the itemsRouter object would be accessible by any module that imports it.

Define each controller function by adding the following under the Controller Definitions section:

/**
 * Controller Definitions
 */

// GET items/

itemsRouter.get("/", async (req: Request, res: Response) => {
  try {
    const items: Items = await ItemService.findAll();

    res.status(200).send(items);
  } catch (e) {
    res.status(404).send(e.message);
  }
});

// GET items/:id

itemsRouter.get("/:id", async (req: Request, res: Response) => {
  const id: number = parseInt(req.params.id, 10);

  try {
    const item: Item = await ItemService.find(id);

    res.status(200).send(item);
  } catch (e) {
    res.status(404).send(e.message);
  }
});


// POST items/

itemsRouter.post("/", async (req: Request, res: Response) => {
  try {
    const item: Item = req.body.item;

    await ItemService.create(item);

    res.sendStatus(201);
  } catch (e) {
    res.status(404).send(e.message);
  }
});

// PUT items/

itemsRouter.put("/", async (req: Request, res: Response) => {
  try {
    const item: Item = req.body.item;

    await ItemService.update(item);

    res.sendStatus(200);
  } catch (e) {
    res.status(500).send(e.message);
  }
});

// DELETE items/:id

itemsRouter.delete("/:id", async (req: Request, res: Response) => {
  try {
    const id: number = parseInt(req.params.id, 10);
    await ItemService.remove(id);

    res.sendStatus(200);
  } catch (e) {
    res.status(500).send(e.message);
  }
});

The logic of the controllers is compact as they delegate the bulk of their operations to the functions of the ItemService module. If you ever need to use a database like MongoDB or PostgreSQL to persist data, you only need to modify the logic of the service provider, the ItemService module, and not the logic of the consumers, your controllers.

To complete the setup of your controllers, you need to wire the itemsRouter with your Express app. To do so, open index.ts and import the router under the Required External Modules section:

/**
 * Required External Modules
 */

import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { itemsRouter } from "./items/items.router";

dotenv.config();

Next, within this file, update the App Configuration section as follows to make use of the router:

/**
 *  App Configuration
 */

app.use(helmet());
app.use(cors());
app.use(express.json());
app.use("/items", itemsRouter);

The app.use() method can take as an argument an optional path and a callback function that represents one or more middleware functions. In this case, you tell your Express app to invoke the itemsRouter middleware functions whenever the /items route path is requested.

Test the Express API Endpoints

With the controllers all set up, it's time to test them out. To make this process simple, you can use cUrl from your terminal, which should work across operating systems.

  • Get all items:
curl http://localhost:7000/items -i

The -i flag includes protocol headers in the output.

You should get an HTTP/1.1 200 OK response from the server and a JSON object that includes three menu items.

  • Get an item:
curl http://localhost:7000/items/2 -i

You should get an HTTP/1.1 200 OK response from the server and a JSON object that includes a menu item defining a pizza 🍕.

  • Add an item:
curl -X POST -H 'Content-Type: application/json' -d '{
  "item": {
    "name": "Salad",
    "price": 4.99,
    "description": "Fresh",
    "image": "https://cdn.auth0.com/blog/whatabyte/salad-sm.png"
  }
}' http://localhost:7000/items -i

You should get an HTTP/1.1 201 Created response from the server and the string Created.

  • Verify that you added the "Salad" menu item:
curl http://localhost:7000/items/ -i

In the server response, the last item in the JSON object should describe a salad and match the data from the POST sent previously.

  • Update an item:
curl -X PUT -H 'Content-Type: application/json' -d '{
  "item": {
    "id": 2,
    "name": "Spicy Pizza",
    "price": 5.99,
    "description": "Blazing Good",
    "image": "https://cdn.auth0.com/blog/whatabyte/pizza-sm.png"
  }
}' http://localhost:7000/items -i

You should get an HTTP/1.1 200 OK response from the server.

  • Verify the item was updated:
curl http://localhost:7000/items/2 -i

You should get an HTTP/1.1 200 OK response from the server and a JSON object showing an updated pizza menu item 🔥🍕.

  • Delete an item:
curl -X DELETE http://localhost:7000/items/2 -i

You should get an HTTP/1.1 200 OK response from the server.

  • Verify that you deleted the item:
curl http://localhost:7000/items/ -i

You should get an HTTP/1.1 200 OK response from the server and a JSON object with three menu items that do not include a pizza 😭🍕.

Handle Errors in Express Using Middleware

In Express, the order in which you declare and invoke middleware is essential for the architecture of your application. What should happen when a client makes a server request that doesn't match any of the server routes? The ideal behavior is to respond to the client with a 400 Bad Request status code.

A good way to handle this is to create an HttpException class that helps you encapsulate errors related to HTTP requests and a middleware function to help you manage and issue the error response.

To continue having an organized application architecture, create a common directory under the src directory to host any classes or interfaces that you use across different project modules:

mkdir src/common

Within the src/common directory, create a file to define the HttpException class:

touch src/common/http-exception.ts

Populate src/common/http-exception.ts as follows:

export default class HttpException extends Error {
  statusCode: number;
  message: string;
  error: string | null;

  constructor(statusCode: number, message: string, error?: string) {
    super(message);

    this.statusCode = statusCode;
    this.message = message;
    this.error = error || null;
  }
}

With this helper class in place, proceed to create a middleware directory under the src directory to host all the files that define custom middleware functions:

mkdir src/middleware

To start, define a middleware function that handles request errors in an error.middleware.ts file inside src/middleware as follows:

touch src/middleware/error.middleware.ts

Now, populate src/middleware/error.middleware.ts with this:

import HttpException from "../common/http-exception";
import { Request, Response, NextFunction } from "express";

export const errorHandler = (
  error: HttpException,
  request: Request,
  response: Response,
  next: NextFunction
) => {
  const status = error.statusCode || 500;
  const message =
    error.message || "It's not you. It's us. We are having some problems.";

  response.status(status).send(message);
};

Here, you receive an error of type HttpException and return an appropriate error based on its properties. If error.status and error.message are defined, you include those in the server response. Otherwise, you default to a generic 500 Internal Server Error status code and a generic message.

It's important to note that you must provide four arguments to identify a function as an error-handling middleware function in Express. You must specify the next object to maintain the error-handling signature even if you don't use it. Otherwise, Express interprets the next object as a regular middleware function and it won't handle any errors.

Now, also consider that the condition of a route not existing is not considered an error by Express when the framework is used to build a RESTful API, as the REST architecture model uses HTTP status codes to provide responses to the client. A missing resource should not be an error but a condition that needs to be reported to the client.

As such, Express won't call your errorHandler middleware function if you request the employees resource, for example:

curl http://localhost:7000/employees/ -i

Executing the request above returns the following generic HTML response:

HTTP/1.1 404 Not Found
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Access-Control-Allow-Origin: *
Content-Security-Policy: default-src 'none'
Content-Type: text/html; charset=utf-8
Content-Length: 149
Date: Thu, 30 Jan 2020 14:51:01 GMT
Connection: keep-alive
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /employees/</pre>
</body>
</html>

To customize how your app responds to undefined resources, create an additional middleware function to catch 404 conditions. Inside the src/middleware directory, create a notFound.middleware.ts file as follows:

touch src/middleware/notFound.middleware.ts

Then, add the following code to src/middleware/notFound.middleware.ts:

import { Request, Response, NextFunction } from "express";

export const notFoundHandler = (
  request: Request,
  response: Response,
  next: NextFunction
) => {

  const message = "Resource not found";

  response.status(404).send(message);
};

You now need to wire these middleware functions with your Express app. Open src/index.ts and update its Required External Modules section to import these error-handling middleware functions:

/**
 * Required External Modules
 */

import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { itemsRouter } from "./items/items.router";
import { errorHandler } from "./middleware/error.middleware";
import {notFoundHandler} from "./middleware/notFound.middleware";

dotenv.config();

Next, update the App Configuration section to mount the errorHandler and notFoundHandler functions:

/**
 *  App Configuration
 */

app.use(helmet());
app.use(cors());
app.use(express.json());
app.use("/items", itemsRouter);

app.use(errorHandler);
app.use(notFoundHandler);

Your application can't reach any routes that you define after mounting the errorHandler middleware function because you close the request-response cycle within errorHandler by sending a response to the client. As such, the errorHandler middleware function must be mounted after all the controller functions of your application have been mounted.

But, as noted earlier, errorHandler won't catch 404 errors. However, you can catch these errors by making notFoundHandler the last middleware function that you mount, which effectively creates a catch-all handler for your app.

Make the following request and observe that you now get your custom 404 response, Resource not found, instead of the generic HTML one:

curl http://localhost:7000/employees/ -i

Now that you have a working CRUD API with error handling, it's time for you to learn how to protect it against unauthorized users.

I'm ready to secure the endpoints of my Express API