Skip to content
Learni
View all tutorials
Développement Backend

How to Build a REST API with Express.js in 2026

Lire en français

Introduction

In 2026, REST APIs remain the backbone of modern web apps, bridging frontends and backends to exchange JSON data. Whether you're building a mobile app, e-commerce site, or dashboard, mastering REST API creation is essential for any backend developer.

This beginner tutorial guides you step-by-step to build a complete REST API handling full CRUD (Create, Read, Update, Delete) on a todo list stored in memory for simplicity. We'll use Node.js and Express.js, the lightweight and battle-tested combo.

Why it matters: A solid API follows REST conventions (HTTP methods, status codes, resources), scales well, and stays secure. At the end, you'll have a testable server using tools like Postman or curl. Estimated time: 30 minutes. Ready to code? (128 words)

Prerequisites

  • Node.js 20+ installed (download it here)
  • Basic JavaScript knowledge (variables, functions, arrays)
  • A code editor like VS Code
  • Testing tools: curl (terminal) or Postman
  • Terminal/Command Prompt

Initialize the project

terminal
mkdir rest-api-todos
cd rest-api-todos
npm init -y
npm install express
npm install --save-dev nodemon

These commands create a project folder, initialize npm with a default package.json, install Express for the HTTP server, and Nodemon for auto-restarting the dev server. Run them in your terminal for a ready setup in 1 minute. Don't skip Nodemon—without it, you'll need to manually restart after every change.

Understand the project structure

Your folder now has package.json, node_modules, and scripts to run npm run dev with Nodemon. We'll use a single server.js file for the entire server—perfect for beginners. Each step updates this file with complete, working code. Always test with node server.js or npm run dev.

Basic server with health check route

server.js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'OK', message: 'Serveur REST API opérationnel' });
});

app.listen(PORT, () => {
  console.log(`Serveur démarré sur http://localhost:${PORT}`);
});

This code sets up an Express server on port 3000, parses incoming JSON, and exposes a GET /health route to check status. The express.json() middleware is essential for JSON bodies. Test with curl http://localhost:3000/health. Pitfall: Without app.use(express.json()), POST requests will fail silently.

Add data and GET /todos route

server.js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

let todos = [
  { id: 1, title: 'Apprendre REST API', completed: false },
  { id: 2, title: 'Coder une todo app', completed: true }
];

app.get('/health', (req, res) => {
  res.json({ status: 'OK', message: 'Serveur REST API opérationnel' });
});

app.get('/todos', (req, res) => {
  res.json(todos);
});

app.listen(PORT, () => {
  console.log(`Serveur démarré sur http://localhost:${PORT}`);
});

We add an in-memory todos array (great for prototypes) and a GET /todos route returning all tasks. Follows REST: GET reads a collection. Test with curl http://localhost:3000/todos. Think of it like an online catalog. Avoid global vars in production; this is for simplicity.

Implement CREATE (POST)

Next: POST /todos. We'll validate input (title required) and generate a unique ID using array length +1. Use status 201 for successful creation.

POST /todos route to create a todo

server.js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

let todos = [
  { id: 1, title: 'Apprendre REST API', completed: false },
  { id: 2, title: 'Coder une todo app', completed: true }
];

app.get('/health', (req, res) => {
  res.json({ status: 'OK', message: 'Serveur REST API opérationnel' });
});

app.get('/todos', (req, res) => {
  res.json(todos);
});

app.post('/todos', (req, res) => {
  const { title } = req.body;
  if (!title || title.trim() === '') {
    return res.status(400).json({ error: 'Le titre est requis' });
  }
  const newTodo = {
    id: todos.length + 1,
    title: title.trim(),
    completed: false
  };
  todos.push(newTodo);
  res.status(201).json(newTodo);
});

app.listen(PORT, () => {
  console.log(`Serveur démarré sur http://localhost:${PORT}`);
});

The POST route validates the body, adds a todo, and returns the created object with 201. Use curl -X POST -H 'Content-Type: application/json' -d '{"title":"New task"}' http://localhost:3000/todos. Pitfall: No validation means junk data pollutes your API. Incremental ID works for demos; use UUIDs in production.

READ/UPDATE/DELETE routes by ID

server.js
const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

let todos = [
  { id: 1, title: 'Apprendre REST API', completed: false },
  { id: 2, title: 'Coder une todo app', completed: true }
];

app.get('/health', (req, res) => {
  res.json({ status: 'OK', message: 'Serveur REST API opérationnel' });
});

app.get('/todos', (req, res) => {
  res.json(todos);
});

app.post('/todos', (req, res) => {
  const { title } = req.body;
  if (!title || title.trim() === '') {
    return res.status(400).json({ error: 'Le titre est requis' });
  }
  const newTodo = {
    id: todos.length + 1,
    title: title.trim(),
    completed: false
  };
  todos.push(newTodo);
  res.status(201).json(newTodo);
});

app.get('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) {
    return res.status(404).json({ error: 'Todo non trouvée' });
  }
  res.json(todo);
});

app.put('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todoIndex = todos.findIndex(t => t.id === id);
  if (todoIndex === -1) {
    return res.status(404).json({ error: 'Todo non trouvée' });
  }
  const { title, completed } = req.body;
  if (title !== undefined) todos[todoIndex].title = title.trim();
  if (completed !== undefined) todos[todoIndex].completed = Boolean(completed);
  res.json(todos[todoIndex]);
});

app.delete('/todos/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const todoIndex = todos.findIndex(t => t.id === id);
  if (todoIndex === -1) {
    return res.status(404).json({ error: 'Todo non trouvée' });
  }
  todos.splice(todoIndex, 1);
  res.status(204).send();
});

app.listen(PORT, () => {
  console.log(`Serveur démarré sur http://localhost:${PORT}`);
});

Adds GET/PUT/DELETE /todos/:id routes with 404 handling. req.params.id captures the URL ID. PUT supports partial updates, DELETE returns 204 (no content). Test e.g. curl -X DELETE http://localhost:3000/todos/1. Like a full CRUD task manager. Pitfall: Always use parseInt to avoid string vs number mismatches.

Update package.json for development

package.json
{
  "name": "rest-api-todos",
  "version": "1.0.0",
  "description": "REST API Todos avec Express.js",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "nodemon": "^3.1.4"
  }
}

This complete package.json adds start and dev scripts. Run with npm run dev for auto-reload. Key for smooth dev workflow. Copy-paste to replace yours.

Test the complete API

Your API is ready! Curl examples:

  • GET all: curl http://localhost:3000/todos
  • POST: curl -X POST -H 'Content-Type: application/json' -d '{"title":"Test"}' http://localhost:3000/todos
  • GET id: curl http://localhost:3000/todos/1
  • PUT: curl -X PUT -H 'Content-Type: application/json' -d '{"completed":true}' http://localhost:3000/todos/1
  • DELETE: curl -X DELETE http://localhost:3000/todos/1
Use Postman for a GUI.

Best practices

  • HTTP status codes: 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Error.
  • Input validation: Always check bodies/params with libs like Joi or Zod.
  • CORS: Add npm i cors and app.use(cors()) for external frontends.
  • Logging: Use Morgan (npm i morgan) to log requests.
  • Environment: Use .env vars with dotenv for PORT/DB in production.

Common errors to avoid

  • Forgetting express.json(): POSTs can't read bodies.
  • No 404 handling: Add app.use((req,res)=>res.status(404).json({error:'Route not found'})) at the end.
  • Non-numeric IDs: Always parseInt(req.params.id) to prevent mismatches.
  • Persistent data: In-memory resets on restart; upgrade to MongoDB/Prisma next.

Next steps

  • Add a database: Tutorial on Prisma with PostgreSQL.
  • Authentication: JWT with jsonwebtoken.
  • Auto-docs: Swagger (npm i swagger-ui-express).
Check out our Learni Group courses for advanced Node.js, TypeScript, and scalable architectures. Share your API on GitHub!