API Routes
Créez des APIs complètes directement dans Next.js
10-15 min
API Routes Next.js
Les API Routes de Next.js permettent de créer des endpoints API directement dans votre application, transformant Next.js en solution full-stack complète.
Introduction aux API Routes
Concept de base
Les API Routes sont des fonctions serverless qui s’exécutent côté serveur. Chaque fichier dans pages/api/
devient un endpoint API.
pages/
└── api/
├── hello.js → /api/hello
├── users/
│ ├── index.js → /api/users
│ └── [id].js → /api/users/123
└── auth/
├── login.js → /api/auth/login
└── logout.js → /api/auth/logout
Premier endpoint
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({
message: 'Hello from Next.js API!',
timestamp: new Date().toISOString()
})
}
Test avec curl :
curl http://localhost:3000/api/hello
Gestion des méthodes HTTP
Endpoint multi-méthodes
// pages/api/users/index.js
export default function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
return getUsers(req, res)
case 'POST':
return createUser(req, res)
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
async function getUsers(req, res) {
try {
// Récupérer les utilisateurs depuis la base de données
const users = await db.user.findMany()
res.status(200).json(users)
} catch (error) {
res.status(500).json({ error: 'Erreur serveur' })
}
}
async function createUser(req, res) {
try {
const { name, email } = req.body
// Validation
if (!name || !email) {
return res.status(400).json({
error: 'Nom et email requis'
})
}
// Créer l'utilisateur
const user = await db.user.create({
data: { name, email }
})
res.status(201).json(user)
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la création' })
}
}
Routes dynamiques
// pages/api/users/[id].js
export default function handler(req, res) {
const { method, query: { id } } = req
switch (method) {
case 'GET':
return getUserById(req, res, id)
case 'PUT':
return updateUser(req, res, id)
case 'DELETE':
return deleteUser(req, res, id)
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
async function getUserById(req, res, id) {
try {
const user = await db.user.findUnique({
where: { id: parseInt(id) }
})
if (!user) {
return res.status(404).json({ error: 'Utilisateur non trouvé' })
}
res.status(200).json(user)
} catch (error) {
res.status(500).json({ error: 'Erreur serveur' })
}
}
Middleware et authentification
Middleware personnalisé
// lib/middleware.js
export function withAuth(handler) {
return async (req, res) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'Token manquant' })
}
// Vérifier le token
const decoded = jwt.verify(token, process.env.JWT_SECRET)
req.user = decoded
return handler(req, res)
} catch (error) {
return res.status(401).json({ error: 'Token invalide' })
}
}
}
Authentification JWT
Login endpoint
// pages/api/auth/login.js
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Méthode non autorisée' })
}
try {
const { email, password } = req.body
// Validation
if (!email || !password) {
return res.status(400).json({ error: 'Email et mot de passe requis' })
}
// Trouver l'utilisateur
const user = await db.user.findUnique({
where: { email }
})
if (!user) {
return res.status(401).json({ error: 'Identifiants invalides' })
}
// Vérifier le mot de passe
const isValidPassword = await bcrypt.compare(password, user.password)
if (!isValidPassword) {
return res.status(401).json({ error: 'Identifiants invalides' })
}
// Générer le token JWT
const token = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '7d' }
)
// Retourner le token et les infos utilisateur
res.status(200).json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role
}
})
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la connexion' })
}
}
Upload de fichiers
// pages/api/upload.js
import multer from 'multer'
import { promisify } from 'util'
// Configuration de multer
const upload = multer({
storage: multer.diskStorage({
destination: './public/uploads',
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, file.fieldname + '-' + uniqueSuffix + '.' + file.originalname.split('.').pop())
}
}),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true)
} else {
cb(new Error('Seules les images sont autorisées'))
}
}
})
const uploadMiddleware = promisify(upload.single('image'))
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Méthode non autorisée' })
}
try {
await uploadMiddleware(req, res)
if (!req.file) {
return res.status(400).json({ error: 'Aucun fichier uploadé' })
}
res.status(200).json({
message: 'Fichier uploadé avec succès',
filename: req.file.filename
})
} catch (error) {
res.status(500).json({ error: error.message })
}
}
// Configuration Next.js pour les uploads
export const config = {
api: {
bodyParser: false,
},
}
Gestion des erreurs
Handler d’erreurs global
// lib/errorHandler.js
export class ApiError extends Error {
constructor(message, statusCode = 500) {
super(message)
this.statusCode = statusCode
this.name = 'ApiError'
}
}
export function withErrorHandler(handler) {
return async (req, res) => {
try {
await handler(req, res)
} catch (error) {
console.error('API Error:', error)
if (error instanceof ApiError) {
return res.status(error.statusCode).json({
error: error.message
})
}
// Erreur générique
res.status(500).json({
error: 'Erreur interne du serveur'
})
}
}
}
CRUD complet avec base de données
// pages/api/posts/index.js
export default async function handler(req, res) {
const { method } = req
switch (method) {
case 'GET':
return getPosts(req, res)
case 'POST':
return createPost(req, res)
default:
res.setHeader('Allow', ['GET', 'POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}
async function getPosts(req, res) {
try {
const { page = 1, limit = 10, search } = req.query
const where = {}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
]
}
const posts = await db.post.findMany({
where,
include: {
author: {
select: { id: true, name: true, email: true }
}
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * limit,
take: parseInt(limit)
})
const total = await db.post.count({ where })
res.status(200).json({
posts,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
})
} catch (error) {
res.status(500).json({ error: 'Erreur lors de la récupération des posts' })
}
}
Validation des données
// lib/validation.js
import { z } from 'zod'
export const userSchema = z.object({
name: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
email: z.string().email('Email invalide'),
password: z.string().min(6, 'Le mot de passe doit contenir au moins 6 caractères')
})
export function validateBody(schema) {
return (handler) => {
return async (req, res) => {
try {
const validatedData = schema.parse(req.body)
req.body = validatedData
return handler(req, res)
} catch (error) {
return res.status(400).json({
error: 'Données invalides',
details: error.errors
})
}
}
}
}
Tests des API Routes
// __tests__/api/users.test.js
import { createMocks } from 'node-mocks-http'
import handler from '../../pages/api/users'
describe('/api/users', () => {
test('GET returns users list', async () => {
const { req, res } = createMocks({
method: 'GET',
})
await handler(req, res)
expect(res._getStatusCode()).toBe(200)
const data = JSON.parse(res._getData())
expect(Array.isArray(data)).toBe(true)
})
test('POST creates new user', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'John Doe',
email: 'john@example.com'
},
})
await handler(req, res)
expect(res._getStatusCode()).toBe(201)
const data = JSON.parse(res._getData())
expect(data.name).toBe('John Doe')
})
})
Bonnes pratiques
1. Structure des réponses
// Réponse de succès standardisée
const successResponse = (data, message = 'Success') => ({
success: true,
message,
data
})
// Réponse d'erreur standardisée
const errorResponse = (message, errors = null) => ({
success: false,
message,
errors
})
2. Rate limiting
// lib/rateLimit.js
import { LRUCache } from 'lru-cache'
const rateLimit = (options = {}) => {
const tokenCache = new LRUCache({
max: options.uniqueTokenPerInterval || 500,
ttl: options.interval || 60000,
})
return {
check: (limit, token) =>
new Promise((resolve, reject) => {
const tokenCount = tokenCache.get(token) || [0]
if (tokenCount[0] === 0) {
tokenCache.set(token, tokenCount)
}
tokenCount[0] += 1
const currentUsage = tokenCount[0]
const isRateLimited = currentUsage >= limit
if (isRateLimited) {
reject(new Error('Rate limit exceeded'))
} else {
resolve({ success: true })
}
}),
}
}
export default rateLimit
3. CORS Configuration
// lib/cors.js
export function withCors(handler) {
return async (req, res) => {
// Configurer CORS
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
if (req.method === 'OPTIONS') {
return res.status(200).end()
}
return handler(req, res)
}
}
Les API Routes de Next.js offrent une solution complète pour créer des backends robustes. Dans le prochain tutoriel, nous verrons comment documenter ces APIs avec Swagger !
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion