DEV Community

Cover image for TypeScript Express: Building Robust APIs with Node.js
Christopher Glikpo  ⭐
Christopher Glikpo ⭐

Posted on

TypeScript Express: Building Robust APIs with Node.js

In today's fast-paced world of web development, building robust and scalable APIs is crucial for any application. Node.js has become a popular choice for backend development due to its non-blocking, event-driven architecture, and the vast ecosystem of libraries and frameworks available. One such framework is Express, which simplifies the process of building web applications and APIs with Node.js.

TypeScript, a statically typed superset of JavaScript, has gained traction among developers for its ability to catch errors during development and provide better tooling and autocompletion. In this blog post, we will explore how to build a robust API using TypeScript and Express, taking advantage of the benefits that TypeScript brings to the table.

1. Setting Up a TypeScript Express Project

To start building an Express API with TypeScript, you'll need to set up your development environment. Follow these steps:

  • Install Node.js and npm:
    If you haven't already, download and install Node.js from the official website (https://nodejs.org). npm (Node Package Manager) comes bundled with Node.js, so once you have Node.js installed, you'll also have npm.

  • Create a new directory for your project:
    Open your terminal or command prompt and navigate to the directory where you want to create your project. You can use the mkdir command to create a new directory. For example:

mkdir my-express-api
Enter fullscreen mode Exit fullscreen mode
  • Navigate to the project directory: Use the cd command to navigate into the newly created directory:
cd my-express-api
Enter fullscreen mode Exit fullscreen mode
  • Initialize a new Node.js project: To initialize a new Node.js project, run the following command in your terminal:
npm init
Enter fullscreen mode Exit fullscreen mode

This command will prompt you to provide information about your project, such as the name, version, description, entry point, etc. You can press enter to accept the default values or provide your own.

  • Install the necessary dependencies: With your project initialized, you need to install the required dependencies. In this case, you'll need Express, TypeScript, ts-node, and the TypeScript declarations for Express.

Run the following command in your terminal to install these dependencies:

npm install express typescript ts-node @types/node @types/express --save-dev
Enter fullscreen mode Exit fullscreen mode

This command will download and install the specified packages and save them as devDependencies in your package.json file.

Express: A minimal and flexible web application framework for Node.js.
TypeScript: A superset of JavaScript that adds static typing and advanced language features.
ts-node: A TypeScript execution environment for Node.js.
@types/express: TypeScript declaration files for Express.
The --save-dev flag ensures that these dependencies are saved as devDependencies, as they are only required during the development process.

  • Configuring TypeScript: Create a tsconfig.json file in the root directory of your project. This file specifies the TypeScript configuration.
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

This configuration specifies the output directory, module system, and other options for the TypeScript compiler.

  • Create a new folder named src and inside it, create an index.ts file. This will be the entry point of your application.

Once the installation is complete, you're ready to start building your TypeScript Express API. You can now proceed to create your TypeScript files and configure the Express application.

  • Note: It's a good practice to create a .gitignore file in your project directory to exclude unnecessary files from version control. Add node_modules to the .gitignore file to prevent it from being tracked by Git.

2. Building an Express API with TypeScript

Now that the project is set up, let's build a simple Express API with TypeScript:

  • Import the necessary dependencies in index.ts:
import express, { Request, Response } from 'express';

const app = express();
const port = process.env.PORT || 3000;
Enter fullscreen mode Exit fullscreen mode
  • Define a route:
app.get('/', (req: Request, res: Response) => {
  res.send('Hello, TypeScript Express!');
});
Enter fullscreen mode Exit fullscreen mode
  • Start the server:
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

This code sets up a basic Express server that listens on port 3000 and responds with a greeting when the root path is accessed.

  • Adding scripts to package.json Add the following scripts to your package.json file:
"scripts": {
  "start": "ts-node src/index.ts",
  "build": "tsc",
  "serve": "node dist/index.js"
}
Enter fullscreen mode Exit fullscreen mode

These scripts allow you to start the development server, build the TypeScript files, and serve the compiled JavaScript files.

  • You can now start your application by running:
npm start
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:3000 in your browser, and you should see the message "Hello, TypeScript Express!".

3. Creating a Simple CRUD API

Now that our basic server is set up, we can start building the API.Let create a simple CRUD (Create, Read, Update, Delete) API for managing a list of tasks.

  • Defining the Task model Create a models directory inside src and add a task.ts file with the following code:
export interface Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
}
Enter fullscreen mode Exit fullscreen mode

This interface defines the structure of a Task object.

  • Implementing the Task API Create a routes directory inside src and add a tasks.ts file with the following code:
import { Router, Request, Response } from 'express';
import { Task } from '../models/task';

const router = Router();
let tasks: Task[] = [];

// Add your CRUD API implementation here

export default router;
Enter fullscreen mode Exit fullscreen mode

Implementing CRUD operations
Now that we have our basic Task API set up, let's implement the CRUD operations.

  • Create a task: Add the following code to the tasks.ts file to create a new task:
router.post('/', (req: Request, res: Response) => {
  const task: Task = {
    id: tasks.length + 1,
    title: req.body.title,
    description: req.body.description,
    completed: false,
  };

  tasks.push(task);
  res.status(201).json(task);
});
Enter fullscreen mode Exit fullscreen mode

This code creates a new task with a unique ID and adds it to the tasks array.

  • Read all tasks: Add the following code to the tasks.ts file to retrieve all tasks:
router.get('/', (req: Request, res: Response) => {
  res.json(tasks);
});
Enter fullscreen mode Exit fullscreen mode
  • Read a single task: Add the following code to the tasks.ts file to retrieve a specific task by ID:
router.get('/:id', (req: Request, res: Response) => {
  const task = tasks.find((t) => t.id === parseInt(req.params.id));

  if (!task) {
    res.status(404).send('Task not found');
  } else {
    res.json(task);
  }
});
Enter fullscreen mode Exit fullscreen mode

This code searches for a task with the specified ID and returns it if found, or a 404 error if not found.

  • Update a task: Add the following code to the tasks.ts file to update a specific task by ID:
router.put('/:id', (req: Request, res: Response) => {
  const task = tasks.find((t) => t.id === parseInt(req.params.id));

  if (!task) {
    res.status(404).send('Task not found');
  } else {
    task.title = req.body.title || task.title;
    task.description = req.body.description || task.description;
    task.completed = req.body.completed || task.completed;

    res.json(task);
  }
});
Enter fullscreen mode Exit fullscreen mode

This code updates the specified task with the new values provided in the request body.

  • Delete a task: Add the following code to the tasks.ts file to delete a specific task by ID:
router.delete('/:id', (req: Request, res: Response) => {
  const index = tasks.findIndex((t) => t.id === parseInt(req.params.id));

  if (index === -1) {
    res.status(404).send('Task not found');
  } else {
    tasks.splice(index, 1);
    res.status(204).send();
  }
});
Enter fullscreen mode Exit fullscreen mode

This code removes the specified task from the tasks array.

Integrating the Task API with the Express server
Finally, let's integrate the Task API with our Express server. Update the index.ts file with the following changes:

import express, { Request, Response } from 'express';
import taskRoutes from './routes/tasks';

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json()); // Add this line to enable JSON parsing in the request body
app.use('/tasks', taskRoutes); // Add this line to mount the Task API routes

app.get('/', (req: Request, res: Response) => {
  res.send('Hello, TypeScript Express!');
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Now, our Task API is fully integrated with the Express server, and we can perform CRUD operations on tasks.

Adding validation and error handling
To further improve our API, let's add validation and error handling to ensure that the data we receive from clients is valid and that we provide meaningful error messages.

  • Installing validation libraries: First, install the express-validator and its type definitions:
npm install express-validator @types/express-validator
Enter fullscreen mode Exit fullscreen mode
  • Adding validation to the Task API: Update the tasks.ts file to include validation for the task creation and update endpoints:
import { Router, Request, Response } from 'express';
import { body, validationResult } from 'express-validator';
import { Task } from '../models/task';

const router = Router();
let tasks: Task[] = [];

const taskValidationRules = [
  body('title').notEmpty().withMessage('Title is required'),
  body('description').notEmpty().withMessage('Description is required'),
  body('completed').isBoolean().withMessage('Completed must be a boolean'),
];

router.post('/', taskValidationRules, (req: Request, res: Response) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

   const task: Task = {
    id: tasks.length + 1,
    title: req.body.title,
    description: req.body.description,
    completed: false,
  };

  tasks.push(task);
  res.status(201).json(task)
});

router.put('/:id', taskValidationRules, (req: Request, res: Response) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }

const task = tasks.find((t) => t.id === parseInt(req.params.id));

  if (!task) {
    res.status(404).send('Task not found');
  } else {
    task.title = req.body.title || task.title;
    task.description = req.body.description || task.description;
    task.completed = req.body.completed || task.completed;

    res.json(task);
  }

});

// ... (rest of the CRUD operations)

export default router;
Enter fullscreen mode Exit fullscreen mode

These changes add validation rules for the title, description, and completed fields and return a 400 Bad Request response with error messages if the validation fails.

  • Adding error handling middleware: To handle errors in a more centralized way, let's add an error handling middleware to our Express server. Update the index.ts file with the following changes:
import express, { Request, Response, NextFunction } from 'express';
import taskRoutes from './routes/tasks';

const app = express();
const port = process.env.PORT || 3000;

app.use(express.json());
app.use('/tasks', taskRoutes);

app.get('/', (req: Request, res: Response) => {
  res.send('Hello, TypeScript Express!');
});

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

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

This middleware will catch any unhandled errors and return a 500 Internal Server Error response.

Conclusion

In this blog post, we have built a robust API using TypeScript and Express by implementing CRUD operations for a Task model. Keep exploring and expanding your API to include even more advanced features, such as authentication, rate limiting, and caching. Happy coding!

Top comments (2)

Collapse
 
sebastian_wessel profile image
Sebastian Wessel

It makes life easier, if you use something like zod.dev

Simply define the schema once and use the generated types.
In your example you do it manually, which means you need to keep validation and types always in sync.

Also, from the schema you can generate OpenApi documentation.

Collapse
 
seanyt profile image
Sean Tarzy

Great tutorial. One thing I'd like to amend is adding cors configuration.

Need to install the package @types/cors

then can import cors from 'cors'

then can app.use(cors());