In today’s interconnected world, APIs (Application Programming Interfaces) are the backbone of modern web applications. They allow different software systems to communicate and exchange data seamlessly. Building a REST API is a fundamental skill for any web developer, enabling you to create powerful and flexible applications. This tutorial will guide you through the process of building a simple REST API using TypeScript, Node.js, and Express, focusing on clarity and practical application for beginners and intermediate developers.
Why Build a REST API?
REST APIs offer a standardized way for applications to interact. They are:
- Scalable: Easily handle increasing traffic and data.
- Flexible: Support various client applications (web, mobile, etc.).
- Maintainable: Easier to update and evolve over time.
This tutorial will teach you how to create a basic API that can handle common HTTP methods (GET, POST, PUT, DELETE) for managing a simple resource. This foundational knowledge is crucial for any developer aiming to build web applications.
Prerequisites
Before we begin, ensure you have the following installed on your system:
- Node.js and npm (Node Package Manager): Used to run JavaScript on your server and manage project dependencies.
- TypeScript: A superset of JavaScript that adds static typing.
- A Code Editor: Such as Visual Studio Code, Sublime Text, or Atom.
Setting Up the Project
Let’s start by creating a new project directory and initializing it with npm. Open your terminal or command prompt and run the following commands:
mkdir typescript-rest-api
cd typescript-rest-api
npm init -y
This will create a `package.json` file, which manages your project’s metadata and dependencies. Next, install the necessary packages:
npm install typescript express @types/express @types/node ts-node --save-dev
Here’s a breakdown of the packages:
- typescript: The TypeScript compiler.
- express: A web application framework for Node.js.
- @types/express: TypeScript definitions for Express.
- @types/node: TypeScript definitions for Node.js.
- ts-node: Allows you to run TypeScript files directly without compiling them first.
Now, let’s create a `tsconfig.json` file to configure TypeScript. Run the following command:
npx tsc --init
This command generates a `tsconfig.json` file with default settings. You might want to modify this file based on your project’s needs. For example, setting the `outDir` to “./dist” to specify the output directory for compiled JavaScript files. Also, set `module` to `commonjs` for compatibility with Node.js and `esModuleInterop` to `true` for better interoperability with ES modules. A simplified `tsconfig.json` file might look like this:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Creating the API Structure
Create a directory named `src` inside your project directory. This is where we’ll keep our TypeScript source files. Inside the `src` directory, create the following files:
- `index.ts`: The main entry point of our application.
- `routes.ts`: Defines our API routes.
- `controllers.ts`: Handles the logic for our API endpoints.
- `models.ts`: Defines the data models.
Your directory structure should look like this:
typescript-rest-api/
├── src/
│ ├── index.ts
│ ├── routes.ts
│ ├── controllers.ts
│ └── models.ts
├── package.json
├── tsconfig.json
└── ...
Defining Data Models (models.ts)
In `models.ts`, we’ll define a simple data model. Let’s create a `Todo` model:
// src/models.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}
This defines a `Todo` interface with `id`, `title`, and `completed` properties. This simple model will represent our data in the API.
Creating Controllers (controllers.ts)
The `controllers.ts` file will contain the functions that handle the logic for our API endpoints. For this example, we’ll create a basic CRUD (Create, Read, Update, Delete) controller for our `Todo` model.
// src/controllers.ts
import { Request, Response } from 'express';
import { Todo } from './models';
// In-memory array to store todos (for simplicity)
let todos: Todo[] = [];
let nextId = 1;
export const getTodos = (req: Request, res: Response) => {
res.json(todos);
};
export const getTodo = (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const todo = todos.find(t => t.id === id);
if (todo) {
res.json(todo);
} else {
res.status(404).json({ message: 'Todo not found' });
}
};
export const createTodo = (req: Request, res: Response) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ message: 'Title is required' });
}
const newTodo: Todo = {
id: nextId++,
title,
completed: false,
};
todos.push(newTodo);
res.status(201).json(newTodo);
};
export const updateTodo = (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
const { title, completed } = req.body;
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
return res.status(404).json({ message: 'Todo not found' });
}
if (title !== undefined) {
todos[todoIndex].title = title;
}
if (completed !== undefined) {
todos[todoIndex].completed = completed;
}
res.json(todos[todoIndex]);
};
export const deleteTodo = (req: Request, res: Response) => {
const id = parseInt(req.params.id, 10);
todos = todos.filter(t => t.id !== id);
res.status(204).send(); // No content
};
This file defines functions for:
- `getTodos`: Retrieves all todos.
- `getTodo`: Retrieves a single todo by ID.
- `createTodo`: Creates a new todo.
- `updateTodo`: Updates an existing todo.
- `deleteTodo`: Deletes a todo.
Note the use of `Request` and `Response` objects from `express`. The example uses an in-memory array `todos` for simplicity. In a real-world application, you’d likely use a database.
Defining Routes (routes.ts)
The `routes.ts` file will define the API endpoints and map them to the corresponding controller functions.
// src/routes.ts
import { Router } from 'express';
import * as controller from './controllers';
const router = Router();
// GET all todos
router.get('/todos', controller.getTodos);
// GET a single todo
router.get('/todos/:id', controller.getTodo);
// POST a new todo
router.post('/todos', controller.createTodo);
// PUT (update) a todo
router.put('/todos/:id', controller.updateTodo);
// DELETE a todo
router.delete('/todos/:id', controller.deleteTodo);
export default router;
This code imports the controller functions and defines routes for the different HTTP methods and paths. For example, `router.get(‘/todos’, controller.getTodos)` maps a GET request to `/todos` to the `getTodos` controller function.
Setting Up the Application (index.ts)
The `index.ts` file is the entry point of your application. It sets up the Express app, defines middleware, and mounts the routes.
// src/index.ts
import express, { Application, Request, Response } from 'express';
import routes from './routes';
import bodyParser from 'body-parser';
const app: Application = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(bodyParser.json()); // Parses JSON request bodies
// Routes
app.use('/api', routes);
// Default route for testing
app.get('/', (req: Request, res: Response) => {
res.send('Hello, TypeScript REST API!');
});
// Start the server
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
This code does the following:
- Imports necessary modules (Express, routes, body-parser).
- Creates an Express application.
- Sets the port (using an environment variable or default 3000).
- Uses `body-parser` middleware to parse JSON request bodies.
- Mounts the routes defined in `routes.ts` under the `/api` path.
- Defines a default route (`/`) for testing.
- Starts the server and listens for incoming requests.
Running the API
To run the API, you need to compile the TypeScript code and then start the Node.js server. Add a script to your `package.json` to simplify this process:
{
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts"
},
"dependencies": {
"express": "^4.18.2",
"body-parser": "^1.20.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.19",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"ts-node-dev": "^2.0.0"
}
}
Here’s a breakdown of the scripts:
- start: This script uses `ts-node` to run the `index.ts` file directly. This is useful for development.
- build: This script compiles the TypeScript code to JavaScript using the `tsc` command. The output will be in the `dist` directory (as configured in `tsconfig.json`).
- dev: This script uses `ts-node-dev`, which provides a development server that automatically restarts the server when changes are detected.
Open your terminal and run the following command to start the development server:
npm run dev
You should see a message in your console indicating that the server is running (e.g., “Server is running on port 3000”).
Testing the API
You can test your API using tools like Postman, Insomnia, or even `curl` from your terminal. Here are some examples:
1. Create a Todo (POST /api/todos)
curl -X POST -H "Content-Type: application/json" -d '{"title": "Learn TypeScript
