Skip to content
Learni
Voir tous les tutoriels
Next.js

Comment créer une REST API avec Next.js en 2026

Read in English

Introduction

En 2026, Next.js domine le développement fullstack grâce à son App Router qui simplifie la création d'API RESTful performantes et serverless. Ce tutoriel vous guide pas à pas pour implémenter une API CRUD complète sur une liste de tâches (todos), avec validation stricte via Zod, gestion d'erreurs HTTP précises et stockage en mémoire persistant entre requêtes (via module global).

Pourquoi cette approche ? Elle exploite les optimisations natives de Next.js (comme les Partial Prerendering implicites), évite les dépendances externes inutiles et prépare au déploiement Vercel en un clic. Idéal pour les apps React modernes où frontend et backend cohabitent. À la fin, vous aurez une API testée, scalable et professionnelle que vous bookmarkerez pour vos projets réels. Prêt à coder ? (142 mots)

Prérequis

  • Node.js 20+ (ou 22 LTS recommandé pour 2026)
  • Connaissances de base en TypeScript et Next.js 14+ (App Router)
  • Éditeur comme VS Code avec extension TypeScript
  • Outils de test : curl ou Postman
  • 10 minutes pour un projet fonctionnel

Initialiser le projet Next.js

terminal
npx create-next-app@latest@canary rest-api-tutorial --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd rest-api-tutorial
npm install zod

Cette commande crée un projet Next.js 15+ (canary pour features 2026) avec TypeScript, Tailwind (optionnel ici), ESLint et App Router. L'alias @/* simplifie les imports. Zod ajoute la validation runtime infaillible, évitant les erreurs de parsing body JSON.

Comprendre la structure App Router

L'App Router place les routes API sous src/app/api/. Chaque dossier route.ts exporte des handlers HTTP (GET, POST, etc.). Les routes dynamiques utilisent [param].

Nous créerons :

  • /api/todos pour lister (GET) et créer (POST).
  • /api/todos/[id] pour lire (GET), updater (PUT) et supprimer (DELETE).

Le stockage utilise un array global dans un module ./lib/store.ts, persistant via Node.js module cache – parfait pour démos, migrez vers DB comme Drizzle pour prod.

Définir les types et schémas Zod

src/lib/schemas.ts
import { z } from 'zod';

export interface Todo {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
}

export const todoSchema = z.object({
  title: z.string().min(3, 'Titre trop court').max(100),
  completed: z.boolean().optional(),
});

export const todoUpdateSchema = z.object({
  title: z.string().min(3).max(100).optional(),
  completed: z.boolean().optional(),
});

Les types TypeScript assurent la complétude IDE, tandis que Zod valide runtime les payloads JSON. todoSchema pour POST, todoUpdateSchema pour PUT (champs optionnels). Piège : toujours .optional() sur updates pour éviter forcer tous les champs.

Créer le store en mémoire

src/lib/store.ts
import type { Todo } from '@/lib/schemas';

let todos: Todo[] = [
  { id: '1', title: 'Apprendre Next.js', completed: false, createdAt: new Date() },
  { id: '2', title: 'Valider avec Zod', completed: true, createdAt: new Date() },
];

export function getTodos(): Todo[] {
  return todos;
}

export function getTodoById(id: string): Todo | undefined {
  return todos.find(todo => todo.id === id);
}

export function createTodo(todoData: Omit<Todo, 'id' | 'createdAt'>): Todo {
  const id = Math.random().toString(36).slice(2);
  const newTodo: Todo = { id, ...todoData, createdAt: new Date() };
  todos.push(newTodo);
  return newTodo;
}

export function updateTodo(id: string, updates: Partial<Todo>): Todo | null {
  const todo = getTodoById(id);
  if (!todo) return null;
  Object.assign(todo, updates);
  return todo;
}

export function deleteTodo(id: string): boolean {
  const index = todos.findIndex(todo => todo.id === id);
  if (index === -1) return false;
  todos.splice(index, 1);
  return true;
}

Ce store simule une DB avec CRUD basique. ID unique via random hex. Partial pour updates flexibles. Piège : en prod, remplacez par mutex (comme pino logger) pour multi-instances Vercel ; ici, module cache suffit pour dev.

Implémenter la route principale /todos

src/app/api/todos/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { todoSchema } from '@/lib/schemas';
import { createTodo, getTodos } from '@/lib/store';

export async function GET() {
  const todos = getTodos();
  return NextResponse.json(todos);
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const validated = todoSchema.parse(body);
    const todo = createTodo(validated);
    return NextResponse.json(todo, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ error: error.errors[0].message }, { status: 400 });
    }
    return NextResponse.json({ error: 'Erreur interne' }, { status: 500 });
  }
}

GET liste tous les todos. POST valide le body Zod, crée et retourne 201. Gestion ZodError pour 400 précis. Piège : toujours await request.json() ; sans try/catch, Zod crash l'API – ici, erreurs granulaires boostent DX.

Route dynamique pour un todo spécifique

Prochaine étape : Créez le dossier src/app/api/todos/[id] et ajoutez route.ts. Elle gère GET/PUT/DELETE par ID, extrait via params.id. Utilisez searchParams si query strings futures.

Route dynamique /todos/[id]

src/app/api/todos/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { todoUpdateSchema } from '@/lib/schemas';
import { deleteTodo, getTodoById, updateTodo } from '@/lib/store';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const todo = getTodoById(params.id);
  if (!todo) {
    return NextResponse.json({ error: 'Todo non trouvé' }, { status: 404 });
  }
  return NextResponse.json(todo);
}

export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const body = await request.json();
    const validated = todoUpdateSchema.parse(body);
    const updated = updateTodo(params.id, validated);
    if (!updated) {
      return NextResponse.json({ error: 'Todo non trouvé' }, { status: 404 });
    }
    return NextResponse.json(updated);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json({ error: error.errors[0].message }, { status: 400 });
    }
    return NextResponse.json({ error: 'Erreur interne' }, { status: 500 });
  }
}

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const deleted = deleteTodo(params.id);
  if (!deleted) {
    return NextResponse.json({ error: 'Todo non trouvé' }, { status: 404 });
  }
  return new NextResponse(null, { status: 204 });
}

GET/PUT/DELETE exploitent params.id. 404 si inexistant, 204 no-content sur DELETE. Zod sur PUT seulement (pas DELETE). Piège : typer { params: { id: string } } strictement – sans, TypeScript loose et runtime crash.

Lancer et tester l'API

terminal
npm run dev

# Nouveau terminal :
curl http://localhost:3000/api/todos
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Nouvelle tâche"}'

curl http://localhost:3000/api/todos/1
curl -X PUT http://localhost:3000/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

curl -X DELETE http://localhost:3000/api/todos/1

Démarre le serveur dev. Ces curls testent full CRUD : listez, créez (201), lisez (200), updatez, supprimez (204). Piège : oubliez Content-Type: application/json et parsing body échoue silencieusement.

Bonnes pratiques

  • Validation systématique : Zod partout pour inputs/outputs, intégrez z.infer pour types auto.
  • Status HTTP précis : 201 create, 204 delete, 400/404/500 granulaires – outils comme Postman adorent.
  • Rate limiting : Ajoutez upstash/ratelimit pour prod anti-abus.
  • Logging : Intégrez pino dans middlewares pour tracer erreurs.
  • Déploiement : vercel --prod direct, avec env vars pour DB réelle.

Erreurs courantes à éviter

  • Oubli params typing : Route dynamique crash sans { params: { id: string } }.
  • Pas de try/catch Zod : Erreurs validation polluent logs 500.
  • Stockage global sans mutex : Multi-calls concurrents corrompent data (use async-mutex).
  • CORS manquant en prod : Ajoutez next.config.js headers pour frontend externe.

Pour aller plus loin

Passez à une DB réelle avec Drizzle ORM + Turso. Auth via NextAuth v6. Optimisez avec React Server Components pour hybrid fullstack.

Découvrez nos formations Learni Dev sur Next.js avancé et architectures scalables. Contribuez sur GitHub pour ce tuto !