Créer une API REST avec Node.js en 30 Minutes (Guide 2026)
Meta Description: Tutoriel complet pour créer une API REST avec Node.js, Express et TypeScript en 2026. De zéro à production en 30 minutes avec exemples de code prêts à l'emploi.Créer une API REST avec Node.js — De Zéro à Production en 30 Minutes
Node.js est le runtime #1 pour les APIs en 2026. Rapide, scalable, et avec un écosystème NPM de 2 millions+ de packages — c'est le choix évident.
Dans ce tutoriel, on va construire une API REST complète en 30 minutes chrono. Pas de blabla théorique : du code, des résultats.
Ce qu'on va construire
Une API de gestion d'utilisateurs avec :
- ✅ CRUD complet (Create, Read, Update, Delete)
- ✅ Validation des données
- ✅ Gestion d'erreurs propre
- ✅ TypeScript
- ✅ Base de données (SQLite pour démarrer, PostgreSQL pour la prod)
- ✅ Documentation automatique
Minute 0-5 : Setup du projet
Initialisation
mkdir my-api && cd my-api
npm init -y
npm install express cors helmet dotenv
npm install -D typescript @types/express @types/node @types/cors tsx
npx tsc --init
Configuration TypeScript
Modifiez tsconfig.json :
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/*/"]
}
Structure du projet
my-api/
├── src/
│ ├── index.ts # Entry point
│ ├── routes/
│ │ └── users.ts # Routes utilisateurs
│ ├── controllers/
│ │ └── users.ts # Logique métier
│ ├── middleware/
│ │ └── errorHandler.ts
│ └── types/
│ └── index.ts
├── package.json
└── tsconfig.json
Minute 5-10 : Le serveur Express
src/index.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { userRoutes } from './routes/users';
import { errorHandler } from './middleware/errorHandler';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Routes
app.use('/api/users', userRoutes);
// Error handling (toujours en dernier)
app.use(errorHandler);
app.listen(PORT, () => {
console.log(🚀 API running on http://localhost:${PORT});
});
src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
export class AppError extends Error {
constructor(
public statusCode: number,
public message: string
) {
super(message);
}
}
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
});
}
console.error('Unexpected error:', err);
res.status(500).json({
error: 'Internal server error',
});
}
Minute 10-15 : Les types et la validation
src/types/index.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserDto {
name: string;
email: string;
role?: 'admin' | 'user';
}
export interface UpdateUserDto {
name?: string;
email?: string;
role?: 'admin' | 'user';
}
Validation simple (sans librairie externe)
export function validateCreateUser(data: unknown): CreateUserDto {
const body = data as Record<string, unknown>;
if (!body.name || typeof body.name !== 'string' || body.name.length < 2) {
throw new AppError(400, 'Name is required (min 2 characters)');
}
if (!body.email || typeof body.email !== 'string' || !body.email.includes('@')) {
throw new AppError(400, 'Valid email is required');
}
if (body.role && !['admin', 'user'].includes(body.role as string)) {
throw new AppError(400, 'Role must be "admin" or "user"');
}
return {
name: body.name,
email: body.email,
role: (body.role as 'admin' | 'user') || 'user',
};
}
Minute 15-22 : Le CRUD complet
src/controllers/users.ts
import { Request, Response, NextFunction } from 'express';
import { User, CreateUserDto, UpdateUserDto } from '../types';
import { AppError } from '../middleware/errorHandler';
// In-memory store (remplacer par DB en prod)
let users: User[] = [
{
id: 1,
name: 'Alice Martin',
email: 'alice@example.com',
role: 'admin',
createdAt: new Date(),
updatedAt: new Date(),
},
];
let nextId = 2;
// GET /api/users
export function getUsers(req: Request, res: Response) {
const { role, search } = req.query;
let result = [...users];
if (role) {
result = result.filter((u) => u.role === role);
}
if (search) {
const s = (search as string).toLowerCase();
result = result.filter(
(u) => u.name.toLowerCase().includes(s) || u.email.toLowerCase().includes(s)
);
}
res.json({
data: result,
total: result.length,
});
}
// GET /api/users/:id
export function getUserById(req: Request, res: Response, next: NextFunction) {
const user = users.find((u) => u.id === parseInt(req.params.id));
if (!user) {
return next(new AppError(404, 'User not found'));
}
res.json({ data: user });
}
// POST /api/users
export function createUser(req: Request, res: Response, next: NextFunction) {
try {
const dto = validateCreateUser(req.body);
// Check duplicate email
if (users.some((u) => u.email === dto.email)) {
return next(new AppError(409, 'Email already exists'));
}
const user: User = {
id: nextId++,
...dto,
role: dto.role || 'user',
createdAt: new Date(),
updatedAt: new Date(),
};
users.push(user);
res.status(201).json({ data: user });
} catch (err) {
next(err);
}
}
// PUT /api/users/:id
export function updateUser(req: Request, res: Response, next: NextFunction) {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) {
return next(new AppError(404, 'User not found'));
}
const updates: UpdateUserDto = {};
if (req.body.name) updates.name = req.body.name;
if (req.body.email) updates.email = req.body.email;
if (req.body.role) updates.role = req.body.role;
users[index] = {
...users[index],
...updates,
updatedAt: new Date(),
};
res.json({ data: users[index] });
}
// DELETE /api/users/:id
export function deleteUser(req: Request, res: Response, next: NextFunction) {
const index = users.findIndex((u) => u.id === parseInt(req.params.id));
if (index === -1) {
return next(new AppError(404, 'User not found'));
}
users.splice(index, 1);
res.status(204).send();
}
function validateCreateUser(data: unknown): CreateUserDto {
const body = data as Record<string, unknown>;
if (!body.name || typeof body.name !== 'string' || body.name.length < 2) {
throw new AppError(400, 'Name is required (min 2 characters)');
}
if (!body.email || typeof body.email !== 'string' || !body.email.includes('@')) {
throw new AppError(400, 'Valid email is required');
}
return {
name: body.name,
email: body.email,
role: (body.role as 'admin' | 'user') || undefined,
};
}
src/routes/users.ts
import { Router } from 'express';
import {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser,
} from '../controllers/users';
export const userRoutes = Router();
userRoutes.get('/', getUsers);
userRoutes.get('/:id', getUserById);
userRoutes.post('/', createUser);
userRoutes.put('/:id', updateUser);
userRoutes.delete('/:id', deleteUser);
Minute 22-27 : Ajouter une base de données (Prisma)
Installation
npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Créer la base
npx prisma migrate dev --name init
Utiliser Prisma dans le controller
Remplacez l'in-memory store par :
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function getUsers(req: Request, res: Response) {
const users = await prisma.user.findMany({
where: {
...(req.query.role ? { role: req.query.role as string } : {}),
...(req.query.search
? {
OR: [
{ name: { contains: req.query.search as string } },
{ email: { contains: req.query.search as string } },
],
}
: {}),
},
});
res.json({ data: users, total: users.length });
}
export async function createUser(req: Request, res: Response, next: NextFunction) {
try {
const dto = validateCreateUser(req.body);
const user = await prisma.user.create({ data: dto });
res.status(201).json({ data: user });
} catch (err: any) {
if (err.code === 'P2002') {
return next(new AppError(409, 'Email already exists'));
}
next(err);
}
}
Minute 27-30 : Scripts et lancement
package.json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
}
}
Lancer l'API
npm run dev
Tester
# Health check
curl http://localhost:3000/health
Créer un utilisateur
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Jean Dupont", "email": "jean@example.com"}'
Lister les utilisateurs
curl http://localhost:3000/api/users
Chercher
curl "http://localhost:3000/api/users?search=jean"
Mettre à jour
curl -X PUT http://localhost:3000/api/users/1 \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
Supprimer
curl -X DELETE http://localhost:3000/api/users/1
Aller plus loin : Production-ready
Ajouter l'authentification (JWT)
npm install jsonwebtoken bcrypt
npm install -D @types/jsonwebtoken @types/bcrypt
import jwt from 'jsonwebtoken';
// Middleware d'authentification
export function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return next(new AppError(401, 'Authentication required'));
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch {
next(new AppError(401, 'Invalid token'));
}
}
Rate limiting
npm install express-rate-limit
import rateLimit from 'express-rate-limit';
app.use(
rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100,
message: { error: 'Too many requests' },
})
);
Logging
npm install morgan
import morgan from 'morgan';
app.use(morgan('combined'));
Docker
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist/ ./dist/
COPY prisma/ ./prisma/
RUN npx prisma generate
EXPOSE 3000
CMD ["node", "dist/index.js"]
Architecture recommandée pour la production
src/
├── index.ts # Entry point
├── config/
│ └── env.ts # Environment variables
├── routes/
│ ├── index.ts # Route aggregator
│ ├── users.ts
│ └── auth.ts
├── controllers/
│ ├── users.ts
│ └── auth.ts
├── services/
│ ├── users.ts # Business logic
│ └── auth.ts
├── middleware/
│ ├── auth.ts
│ ├── errorHandler.ts
│ └── validate.ts
├── types/
│ └── index.ts
└── utils/
└── logger.ts
Principe : Routes → Controllers → Services → Database
Chaque couche a une responsabilité claire. Les controllers ne font que de la plomberie HTTP. La logique métier est dans les services.
Résumé
En 30 minutes, vous avez :
| Étape | Durée | Résultat |
|-------|-------|---------|
| Setup | 5 min | Projet TypeScript prêt |
| Serveur | 5 min | Express avec middleware |
| Types | 5 min | Validation des données |
| CRUD | 7 min | API fonctionnelle |
| Database | 5 min | Prisma + SQLite |
| Launch | 3 min | API en production |
Besoin d'aller plus vite ?
Construire une API de zéro, c'est formateur. Mais pour un projet réel, vous avez besoin de bien plus : auth, paiements, mailing, admin panel, déploiement...
Notre SaaS Boilerplate inclut tout ça — React frontend + Symfony backend + Docker — prêt à personnaliser et déployer.
👉 Voir le SaaS Boilerplate — 99€
Ou le DevOps Starter Kit pour dockeriser et déployer automatiquement votre API Node.js.
FAQ
Express ou Fastify en 2026 ?
Express reste le standard. Fastify est plus rapide (2-3x) mais avec un écosystème plus petit. Pour la plupart des projets, Express suffit largement.
Faut-il TypeScript pour une API Node.js ?
Fortement recommandé. Le typage statique catch des bugs avant le runtime et améliore l'autocompletion. Le surcoût de setup est minimal avec tsx.
SQLite en production ?
Pour des side projects ou des apps avec < 1000 requêtes/seconde, oui. Au-delà, migrez vers PostgreSQL (il suffit de changer le provider Prisma).
Comment tester mon API ?
Utilisez Postman, Insomnia, ou simplement curl. Pour les tests automatisés : supertest + jest ou vitest.
Combien d'utilisateurs mon API peut-elle gérer ?
Node.js gère facilement 10 000+ requêtes/seconde sur un seul core. Avec un cluster ou PM2, 50 000+. La DB est généralement le bottleneck.
REST ou GraphQL ?
REST pour 90% des projets. GraphQL quand vous avez des clients mobiles avec des besoins de données très variés ou des relations de données complexes.