Building a RESTful API with TypeScript, Node.js and Express

Building a RESTful API with TypeScript, Node.js and Express

7 min read

Table of Contents

Introduction

RESTful APIs allow for the creation of scalable, flexible, and maintainable web applications. In this tutorial, we will be building a simple RESTful API using TypeScript and Node.js with the Express framework.

Before we get started, make sure you have Node.js and npm (which comes with Node.js) installed on your machine.

To set up a development environment with TypeScript, you will need to install the TypeScript compiler and the ts-node package, which allows us to run TypeScript scripts directly from the command line. Open up your terminal and run the following command:

npm install -g typescript ts-node

Next, create a new directory for your project and navigate into it. Then, run the following command to initialize a new npm project:

npm init -y

This will create a `package.json“ file in your project directory.

Now, we can install the dependencies we need for our project. Run the following command to install Express, Node.js, and the required TypeScript types:

npm install express node @types/node @types/express

Creating a Basic Express Server

With our dependencies installed, we can now create a basic Express server. Create a new file called server.ts in your project directory and add the following code:

import express from 'express';

const app = express();

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

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Listening on port ${port}...`);
});

This code creates a new Express app and adds a simple route that sends a “Hello, world!” message when a GET request is made to the root URL (/). The server will listen on port 3000 by default, or use the port specified in the PORT environment variable if it is set.

To start the server, run the following command in your terminal:

ts-node server.ts

You should see the following output in your terminal:

Listening on port 3000...

You can now make a GET request to the root URL of your server to see the “Hello, world!” message:

curl http://localhost:3000

You should see the following output:

Hello, world!

Adding Endpoints and Handling HTTP Verbs

Now that we have a basic server set up, let’s add some endpoints to it. We’ll start by adding some in-memory data to store our users. Add the following code to your server.ts file:

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

let nextId = 4;

We can now add some routes to our server to allow us to perform CRUD (create, read, update, delete) operations on our user data. Add the following code to your server.ts file:

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

app.get('/users/:id', (req, res) => {
  const user = users.find((u) => u.id === parseInt(req.params.id));
  if (!user) {
    res.status(404).send('User not found');
  } else {
    res.send(user);
  }
});

app.post('/users', (req, res) => {
  const user = { id: nextId++, name: req.body.name };
  users.push(user);
  res.send(user);
});

app.put('/users/:id', (req, res) => {
  const user = users.find((u) => u.id === parseInt(req.params.id));
  if (!user) {
    res.status(404).send('User not found');
  } else {
    user.name = req.body.name;
    res.send(user);
  }
});

app.delete('/users/:id', (req, res) => {
  const userIndex = users.findIndex((u) => u.id === parseInt(req.params.id));
  if (userIndex === -1) {
    res.status(404).send('User not found');
  } else {
    users.splice(userIndex, 1);
    res.send('Deleted');
  }
});

These routes allow us to perform CRUD (create, read, update, delete) operations on our user data. The /users route returns a list of all users, the /users/:id route returns a single user with the specified ID, the /users route with a POST request creates a new user, the /users/:id route with a PUT request updates a user, and the /users/:id route with a DELETE request deletes a user.

To test these routes, we can use the curl command again. For example, to get a list of all users, run the following command:

curl http://localhost:3000/users

You should see the following output:

[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]

To get a single user, run the following command:

curl http://localhost:3000/users/1

You should see the following output:

{"id":1,"name":"Alice"}

To create a new user, run the following command:

curl -X POST -H "Content-Type: application/json" -d '{"name": "Dave"}' http://localhost:3000/users

You should see the following output:

{"id":4,"name":"Dave"}

We recommend using a tool like Postman to test your API endpoints. Postman allows you to easily make HTTP requests to your API and view the responses.

Validation and Data Modeling with TypeScript

To ensure that our API is robust and maintainable, we should add validation and data modeling to our server. TypeScript’s type system and interfaces can help us with this.

First, let’s define an interface to represent a user. Add the following code to your server.ts file:

interface User {
  id: number;
  name: string;
}

This interface defines the shape of a user object with two properties, id and name.

Next, let’s modify our in-memory data store to use the User interface. Replace the const users and let nextId lines with the following code:

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

let nextId = 4;

Now, let’s add some validation to our routes. Add the following code to your server.ts file:

app.post('/users', (req, res) => {
  if (!req.body.name || req.body.name.trim().length === 0) {
    res.status(400).send('Name is required');
    return;
  }

  const user: User = { id: nextId++, name: req.body.name };
  users.push(user);
  res.send(user);
});

app.put('/users/:id', (req, res) => {
  if (!req.body.name || req.body.name.trim().length === 0) {
    res.status(400).send('Name is required');
    return;
  }

  const user = users.find((u) => u.id === parseInt(req.params.id));
  if (!user) {
    res.status(404).send('User not found');
  } else {
    user.name = req.body.name;
    res.send(user);
  }
});

We’ve added some validation to the POST and PUT routes to ensure that the name field is present and not an empty string.

Handling Errors and Debugging

As we develop our API, we may encounter errors that need to be handled and debugged. Express provides a few ways to do this.

First, let’s add a global error handler to our app. Add the following code to your server.ts file:

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).send('Something went wrong');
});

This error handler will catch any errors that occur in our app and log them to the console. It will also send a “Something went wrong” message to the client.

To test this error handler, let’s add a route that throws an error. Add the following code to your server.ts file:

app.get('/error', (req, res) => {
  throw new Error('Test error');
});

Now, if you navigate to http://localhost:3000/error, you should see the “Something went wrong” message in your browser and the error stack trace in your terminal.

In addition to the global error handler, we can also use the try and catch statements to handle errors in specific routes. For example:

app.get('/users/:id', (req, res) => {
  try {
    const user = users.find((u) => u.id === parseInt(req.params.id));
    if (!user) {
      throw new Error('User not found');
    }
    res.send(user);
  } catch (err) {
    res.status(404).send(err.message);
  }
});

This route will send a 404 status and the error message if the user is not found.

Deployment and Best Practices

Once you have finished developing your API, it’s time to deploy it for others to use. There are many hosting options available, such as Render (render.com). Render offers a free plan for hosting your API, making it a great option for those just starting out.

Before deploying your API, there are a few best practices to follow:

  • Use a process manager like PM2 to keep your server running in the background even if you close the terminal window
  • Set environment variables for sensitive information like API keys to keep them out of version control
  • Use a logging service like Loggly or Splunk to keep track of errors and requests for debugging and analysis
  • Use a monitoring service like New Relic or AppDynamics to track the performance of your API and identify issues

By following these best practices, you can ensure that your API is reliable, maintainable, and scalable.

Conclusion

In this tutorial, we learned how to build a RESTful API using TypeScript and Node.js with the Express framework. We covered setting up a development environment, creating a basic server, adding endpoints and handling HTTP verbs, validation and data modeling with TypeScript, handling errors and debugging, and deployment and best practices. By following the steps in this tutorial, you should now have the knowledge and skills to build your own RESTful API with TypeScript and Node.js.


Comments