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 :

Stack : Node.js 22 + Express + TypeScript + Prisma

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.

👉 Voir le DevOps Kit — 59€


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.