Discover and enable the integrations you need to solve identityAuth0 Marketplace
TypeScript

Node.js and TypeScript Tutorial: Build a CRUD API

Learn how to use TypeScript to build a feature-complete Express API. Tutorial on how to use TypeScript with Express to create, read, update, and delete data.

January 14, 2021

TypeScript

Node.js and TypeScript Tutorial: Build a CRUD API

Learn how to use TypeScript to build a feature-complete Express API. Tutorial on how to use TypeScript with Express to create, read, update, and delete data.

January 14, 2021

This tutorial shows you how to build a feature-complete API using Node.js, Express, and TypeScript that lets clients perform data operations on resources that describe a restaurant menu. Using TypeScript with Node.js gives you access to optional static type-checking along with robust tooling for large apps and the latest ECMAScript features.

Learn also how to define data models, create a data service, and quickly build modular endpoints. As an option, you can also learn how to secure the API using Auth0.

What You Will Build

You'll learn how to create and secure a feature-complete Express API through hands-on practice. You'll test your API locally using terminal commands. Additionally, you'll use a live client application, the "WHATABYTE Dashboard", as a testing harness to simulate production conditions and live user interactions.

WHATBYTE Dashboard demo client

The sleek web player from Spotify inspired the design of the live demo application.

For simplicity, you'll store data in-memory and not in an external database.

For security, you'll limit API access by following these business rules:

  • Anyone can read data.

  • Only authenticated users with a menu-admin role can create, update, or delete menu items. The menu-admin role will bundle the necessary permissions to execute these write operations.

We tested this tutorial using Node.js v14.15.1 and NPM 6.14.8. If you need to install any of them, follow the instructions provided by the Node.js Foundation for your operating system.

Get Starts with TypeScript in Node.js

Bootstrap a Node.js Project

Start by creating a project directory anywhere in your system and making it your current directory:

mkdir menu-api
cd menu-api

Next, initialize a Node.js project within the project directory by creating a package.json file with default settings:

npm init -y

Install Project Dependencies

Your Node.js project requires a couple of dependencies to create a secure Express server with TypeScript. Install them like so:

npm i express dotenv cors helmet

Here's what each of the above packages does in your project:

  • express: Fast, unopinionated, minimalist web framework for Node.js.

  • dotenv: Zero-dependency module that loads environment variables from a .env file into process.env.

  • cors: Express middleware to enable CORS with various options.

  • helmet: Express middleware to secure your apps by setting various HTTP headers, which mitigate common attack vectors.

To use TypeScript, you also need to install a stable version of typescript as a developer dependency:

npm i -D typescript

To use TypeScript effectively, you need to install type definitions for the packages you installed previously:

npm i -D @types/node @types/express @types/dotenv @types/cors @types/helmet

When a package doesn't have built-in types, you can get its type definitions through the @types npm namespace, which hosts TypeScript type definitions from the DefinitelyTyped project. Once you install the packages, the TypeScript compiler automatically includes the types, and your IDE can use them to provide you with code assistance.

We tested this tutorial using Express v4.17.117.1, Node.js v14.15.115.1, and npm 6.14.

Initialize TypeScript in Node.js

To help the TypeScript compiler understand your project's structure, you need to create a tsconfig.json file within the directory you want to use as the root directory of the TypeScript project. In this case, your project directory and the TypeScript project directory are the same.

To easily generate the tsconfig.json file, ensure that you are under the project directory and issue the following command:

npx tsc --init

That's all that you need for now to configure your TypeScript project with sensible defaults.

Use Environmental Variables

Instead of using hard-coded configuration variables within files throughout your project, you can define all those variables in a central location and load them into the file modules that need them. Developers working with Node.js commonly define this central location as a hidden file named .env, which you can create as follows:

touch .env

Populate the .env hidden file with the following variable that defines the port your server can use to listen for requests:

PORT=7000

As seen in the next section, any module of your project can load the variables defined within .env using the dotenv package.

⚠️ Caution: A .env file may contain sensitive information, such as API keys or secrets. Thus, add it to a project's .gitignore file to prevent it from being committed to version control.

Create an Express App with TypeScript

To keep your application well-organized, create an src directory to host your Node.js application files:

mkdir src

Under this src directory, create a file named index.ts to serve as the entry point of the application:

touch src/index.ts

Add to index.ts the following template that outlines an Express server:

/**
 * Required External Modules
 */

/**
 * App Variables
 */

/**
 *  App Configuration
 */

/**
 * Server Activation
 */

Next, under the Required External Modules section, import the project dependencies you installed earlier and load any environmental variables from the local .env file using the dotenv.config() method:

/**
 * Required External Modules
 */

import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import helmet from "helmet";

dotenv.config();

Under the App Variables section, check if Node.js loaded the environmental variable PORT into process.env. If so, parse its value as a number type and create an instance of an Express application; otherwise, exit the application:

/**
 * App Variables
 */

if (!process.env.PORT) {
   process.exit(1);
}

const PORT: number = parseInt(process.env.PORT as string, 10);

const app = express();

Under the App Configuration section, mount the middleware functions from the packages that you are importing into this entry point module:

/**
 *  App Configuration
 */

app.use(helmet());
app.use(cors());
app.use(express.json());

helmet is a collection of 14 small middleware functions that set HTTP response headers. Mounting helmet() doesn't include all of these middleware functions but provides you with sensible defaults such as DNS Prefetch Control, Frameguard, Hide Powered-By, HSTS, IE No Open, Don't Sniff Mimetype, and XSS Filter.

By mounting cors(), you enable all CORS requests. With express.json(), you parse incoming requests with JSON payloads, which populates the request object with a new body object containing the parsed data.

Finally, under the Server Activation section, you create an Express server:

/**
 * Server Activation
 */

app.listen(PORT, () => {
  console.log(`Listening on port ${PORT}`);
});

Improve TypeScript Development Workflow

The TypeScript compilation process can increase the bootstrapping time of an application. However, you don't need to recompile the entire project whenever there's a change in its source code. You can set up ts-node-dev to significantly decrease the time it takes to restart your application when you make a change.

Start by installing this package to power up your development workflow:

npm i -D ts-node-dev

ts-node-dev restarts a target Node.js process when any of the required files change. However, it shares the Typescript compilation process between restarts, which can significantly increase the restart speed.

You can create a dev npm script in package.json to run your server:

{
  "name": "menu-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "ts-node-dev --respawn --pretty --transpile-only src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": { ... },
  "devDependencies": { ... }
}

Let's briefly break down the options that ts-node-dev takes:

  • --respawn: Keep watching for changes after the script has exited.

  • --pretty: Use pretty diagnostic formatter (TS_NODE_PRETTY).

  • --transpile-only: Use TypeScript's faster transpileModule (TS_NODE_TRANSPILE_ONLY).

  • src/index.ts: This is the application's entry file.

Since ts-node-dev is a tweaked version of node-dev that uses ts-node under the hood, some of its options are the combined options of those two packages. Refer to its "Usage" document for more details and available options.

Now, simply run the dev script to launch your project:

npm run dev

If everything is working correctly, you'll see a message indicating that the server is listening for requests on port 7000.

You are ready to develop this application further using TypeScript interfaces to define data models, services to manipulate the API resources, and much more.

Model Data with TypeScript Interfaces

Before creating any controllers and services, define the structure of the data you want to manage. A menu item has the following properties:

  • id: (number) Unique identifier for the item record.
  • name: (string) Name of the item.
  • price: (number) Price of the item in cents.
  • description: (string) Description of the item.
  • image: (string) URL pointing to the item's image.

WHATBYTE Dashboard menu item

Why are you using cents to represent the item's price?

It's better to use integer values to store the item's price and perform arithmetic operations. Developers should never use floating-point numbers to represent monetary values. Floating-point numbers cannot precisely represent all real numbers. As such, floating-point operations cannot precisely represent true arithmetic operations, leading to many surprising situations.

For example, let's say you plan to sell a cup of tea for 1 dollar and 10 cents, $1.10. You have a customer who orders 162 cups. For what amount should you invoice the customer?

1.10 * 162 = 178.20000000000002

You need to apply a rounding function to 178.20000000000002 to get an appropriate invoice amount.

Now, what if you represent the price of a cup of tea in cents? It would be 110 cents, which makes finding the invoice amount much more accurate:

110 * 162 = 17820

All that you have to do now is to divide that number by 100 to display or print the dollar amount:

17820 / 100 = 178.2

Invoice amount: $178.20.

Remember: Store in cents. Display in dollars.

Since you are using TypeScript to build your API, you have different options to define and enforce the structure of objects during development. You can use either classes or interfaces to define custom types. You can use interfaces for the menu items as they are not part of the compiled JavaScript bundle, keeping it smaller, and your application won't need instances of a menu item.

To keep your project organized, create an items directory under the src directory to store all files related to menu items:

mkdir src/items

To start modeling your data, create a file to hold the definition of a BaseItem and an Item type:

touch src/items/item.interface.ts

Populate src/items/item.interface.ts with the following definition:

// src/items/item.interface.ts

export interface BaseItem {
  name: string;
  price: number;
  description: string;
  image: string;
}

export interface Item extends BaseItem {
  id: number;
}

There will be scenarios where you only need to assert the item's structure without its id, such as when you get a payload to create a new item or update an existing one. In those types of requests, POST and PUT, the client sends the item id as a query parameter. For those cases, you'll use the BaseItem interface.

Next, create a file to hold the definition of an Items type, a bundle of elements of type Item:

touch src/items/items.interface.ts

Populate src/items/items.interface.ts as follows:

// src/items/items.interface.ts

import { Item } from "./item.interface";

export interface Items {
  [key: number]: Item;
}

Create a Node.js Service with TypeScript

A service lets you encapsulate related business logic that you can share across multiple projects. As such, your application can use a service to access and manipulate records from your store.

Create a file to define a service module:

touch src/items/items.service.ts

Populate the src/items/items.service.ts file with the following template:

// src/items/items.service.ts

/**
 * Data Model Interfaces
 */


/**
 * In-Memory Store
 */


/**
 * Service Methods
 */

Now, under the Data Model Interfaces section, import the interfaces you created earlier:

/**
 * Data Model Interfaces
 */

import { BaseItem, Item } from "./item.interface";
import { Items } from "./items.interface";

To keep this tutorial simpler, you are not using an external database to store records. Instead, under the In-Memory Store section, you create a simple in-memory store represented as an object:

/**
 * In-Memory Store
 */

let items: Items = {
  1: {
    id: 1,
    name: "Burger",
    price: 599,
    description: "Tasty",
    image: "https://cdn.auth0.com/blog/whatabyte/burger-sm.png"
  },
  2: {
    id: 2,
    name: "Pizza",
    price: 299,
    description: "Cheesy",
    image: "https://cdn.auth0.com/blog/whatabyte/pizza-sm.png"
  },
  3: {
    id: 3,
    name: "Tea",
    price: 199,
    description: "Informative",
    image: "https://cdn.auth0.com/blog/whatabyte/tea-sm.png"
  }
};

It's important to note that anytime that you restart the server, Express wipes the in-memory store. However, since you are using ts-node-dev, that only happens when you make changes to the service module file.

Next, create methods to perform read and write operations on the items store. Start by creating two methods under the Service Methods section to find store elements:

/**
 * Service Methods
 */

export const findAll = async (): Promise<Item[]> => Object.values(items);

export const find = async (id: number): Promise<Item> => items[id];

To simulate the asynchronous nature of read and write operations, all the service methods are async methods. findAll returns the whole items store object, while find receives an id parameter that it uses to look up and return a single store element if found.

Next, define a method to create a new item in the store:

/**
 * Service Methods
 */

export const findAll = async (): Promise<Items> => {...};

export const find = async (id: number): Promise<Item> => {...};

export const create = async (newItem: BaseItem): Promise<Item> => {
  const id = new Date().valueOf();

  items[id] = {
    id,
    ...newItem,
  };

  return items[id];
};

The create method is simple: it receives an object of type BaseItem as an argument, providing all the required values to define a new item in the store, except the item's id. To create a unique id value for each new element added to the store, you use the value of the current Date, based on the number of milliseconds between 1 January 1970 00:00:00 UTC and the current time.

Right below, add a method to update an existing store item:

/**
 * Service Methods
 */

export const findAll = async (): Promise<Item[]> => {...};

export const find = async (id: number): Promise<Item> => {...};

export const create = async (newItem: BaseItem): Promise<Item> => {...};

export const update = async (
  id: number,
  itemUpdate: BaseItem
): Promise<Item | null> => {
  const item = await find(id);

  if (!item) {
    return null;
  }

  items[id] = { id, ...itemUpdate };

  return items[id];
};

The update method receives the item id property and an itemUpdate object as arguments. You use the id to find the item in the store to update it wit the properties of itemUpdate. If the store doesn't have the item, you return null.

Finally, define a method to remove an item from the store:

/**
 * Service Methods
 */

export const findAll = async (): Promise<Item[]> => {...};

export const find = async (id: number): Promise<Item> => {...};

export const create = async (newItem: BaseItem): Promise<Item> => {...};

export const update = async (
  id: number,
  itemUpdate: BaseItem
): Promise<Item | null> => {...};

export const remove = async (id: number): Promise<null | void> => {
  const item = await find(id);

  if (!item) {
    return null;
  }

  delete items[id];
};

The remove method receives an id value as a parameter and uses it to look up an item in the store and to delete it if found.

You've now completed the creation of a nimble service module using TypeScript and Node.js. Any other project can use the code of this service module as it's not tied to any particular framework. You use it in the next section to create your API controllers.

It's worth mentioning that you could have used a TypeScript class to define and encapsulate the service logic; however, using functions makes testing your service module much easier.

Create Express Controllers

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 /api/menu/items

# get a single item using an id parameter
GET /api/menu/items/:id

# create an item
POST /api/menu/items

# update an item using an id parameter
PUT /api/menu/items/:id

# remove an item using an id parameter
DELETE /api/menu/items/:id

You can refer to the WHATABYTE Menu API document to get more details on the signature, input, and response of each endpoint.

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

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/:id

// 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 { BaseItem, Item } from "./item.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, making 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 you can easily test and re-use.

You export the itemsRouter right away, even though you have not defined its routing properties yet. 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: Item[] = await ItemService.findAll();

    res.status(200).send(items);
  } catch (e) {
    res.status(500).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);

    if (item) {
      return res.status(200).send(item);
    }

    res.status(404).send("item not found");
  } catch (e) {
    res.status(500).send(e.message);
  }
});

// POST items

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

    const newItem = await ItemService.create(item);

    res.status(201).json(newItem);
  } catch (e) {
    res.status(500).send(e.message);
  }
});

// PUT items/:id

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

  try {
    const itemUpdate: Item = req.body;

    const existingItem: Item = await ItemService.find(id);

    if (existingItem) {
      const updatedItem = await ItemService.update(id, itemUpdate);
      return res.status(200).json(updatedItem);
    }

    const newItem = await ItemService.create(itemUpdate);

    res.status(201).json(newItem);
  } 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(204);
  } 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("/api/menu/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 api/menu/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.

Are you developing on Windows? Learn more about Windows PowerShell Commands for Web Developers.

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

The -i flag includes protocol headers in the output.

You should get a 200 OK response with an array that includes three menu items.

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

You should get a 200 OK response with a JSON object describing a pizza.

  • Add an item:
curl -X POST -H 'Content-Type: application/json' -d '{
  "name": "Salad",
  "price": 499,
  "description": "Fresh",
  "image": "https://images.ctfassets.net/23aumh6u8s0i/5pnNAeu0kev0P5Neh9W0jj/5b62440be149d0c1a9cb84a255662205/whatabyte_salad-sm.png"
}' http://localhost:7000/api/menu/items -i

You should get an 201 Created response with a JSON object describing a salad.

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

The last item in the JSON object response should describe a salad and match the data from the POST request you sent previously.

  • Update an item:
curl -X PUT -H 'Content-Type: application/json' -d '{
  "name": "Spicy Pizza",
  "price": 599,
  "description": "Blazing Good",
  "image": "https://images.ctfassets.net/23aumh6u8s0i/2x1D2KeepKoZlsUq0SEsOu/bee61947ed648848e99c71ce22563849/whatabyte_pizza-sm.png"
}' http://localhost:7000/api/menu/items/2 -i

You should get a 200 OK response with a JSON object describing the updated menu item.

  • Verify that your API updated the item:
curl http://localhost:7000/api/menu/items/2 -i

You should get a 200 OK response with the updated menu item.

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

You should get an 204 No Content response from the server.

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

You should get a 200 OK response with a JSON object that includes three menu items. None of the items is a pizza.

Implement Express Error Handling

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 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:

// src/common/http-exception.ts

export default class HttpException extends Error {
  statusCode?: number;
  status?: 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:

// src/middleware/error.middleware.ts

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 || error.status || 500;

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

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 you use the framework to build a RESTful API. The REST architecture model uses HTTP status codes to respond to the client. A missing resource should not be an error but a condition you need to report 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 not-found.middleware.ts file as follows:

touch src/middleware/not-found.middleware.ts

Then, add the following code to src/middleware/not-found.middleware.ts:

// src/middleware/not-found.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/not-found.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("/api/menu/items", itemsRouter);

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

Your application can't reach any routes 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, you must mount the errorHandler middleware function after you have mounted all the controller functions of your application.

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

Test the API with the Demo Client

You can use a demo application, the WHATABYTE Dashboard, to interact with your Menu API like any user would. The demo application lets you enable and disable its authentication features.

Since you have not yet implemented authorization in your API to protect your endpoints, you'll use the demo client without any authentication features, which allows you to perform read and write operations on your Menu API as an anonymous user.

Set up the demo client application

Head to https://dashboard.whatabyte.app to open the demo client. If this is your first time using this client application, the Auth0 Demo Settings view will open up.

Under the "Auth0 Demo Settings" view, ensure that the Enable Authentication Features option is off:

Dashboard demo settings without authentication

You should have a form with one field labeled API Server Base URL under its API Configuration section. The value of this field corresponds to your Express server base URL, in this case, http://localhost:7000.

If you are using any other base URL for your server, change the value of the form field.

Now, click the Save button to load the demo client application:

Dashboard home page

Locate the navigation bar on the left side of the page. Then, click on the Menu tab. Three menu items from your server's store should load up:

Dashboard menu page

Create an item

On the "Menu Items" page, click on the Add Item button on the top-right corner. The "Add Menu Item" page should load up with a pre-populated form:

Page to add a menu item

Click on the Save button to add a "Spring Salad" item to your menu.

Once the request-response cycle is complete between the client and the server, the client application loads the "Menu Items" page again. The menu grid now features four items, which includes the "Spring Salad":

Menu page showing new item

Update an item

Now, try updating the property of an item. Click on the "Tea" item to load its item page:

Tea menu item page

You'll notice two buttons at the bottom: Edit and Delete.

Click the Edit button and modify the form that loads up:

  • Change the Name value from "Tea" to "Ginger Tea".
  • Change the Description value from "Informative" to "Energizing".

Page to edit the tea item

Then, click the Save button. Once the request-response cycle completes again, you'll see four items in the menu grid. However, the "Tea" item will show its new name and description:

Updated menu item page

Delete an item

Click on any item on the menu grid, such as the "Spring Salad". On the item page, click its Delete button. You'll load up the "Delete Menu Item" page, asking you to confirm if you what to delete the item:

Page to delete the salad item

Click the Delete button to confirm the operation. After the request-response cycle completes, the menu grid loads up without that particular item:

Menu page without the deleted item

Security Considerations

Now that you have a working API with error handling to read and write data, it's time for you to learn how to protect it against unauthorized access by using Auth0.

Auth0 is a flexible, drop-in solution to add authentication and authorization services to your applications. Your team and organization can avoid the cost, time, and risk that comes with building your solution to authenticate and authorize users. Auth0 offers tons of guidance and SDKs for you to get started and integrate Auth0 in your stack easily.

As it is, anyone could use the client application to perform write operations. Someone could delete all the menu items:

Empty menu page

To prevent such a data catastrophe from happening, you need to secure your write endpoints by implementing authorization on the API layer. Once your Menu API has authorization in place, you can enable the authentication features of the demo client to improve the UX of the end-users.

In the next part of this tutorial, you'll require users to log in to perform write operations on the API. Additionally, you'll further increase your API's security by requiring users to have a set of permissions (defined through a role) to perform any write operation.

Next Step: Next Step: I'm Ready to Secure My Express API

I ran into an issue

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon