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.