0%
API Routes Next.js - Créer des APIs complètes

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 !

Ressources utiles

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !