APIs REST et sorting/filtering

Ce cours fait partiellement suite à celui de Node.

On va voir des manières de structurer nos APIs Rest ainsi que de filtrer et trier les données.

Statut HTTP

2XX : Requête réussie

Ici on est dans le cas où la requête a été réussie. Globalement, on utilisera les codes 200, 201 et 204.

  • 200 : Requête réussie
  • 201 : Entité créée
  • 204 : Requête réussie mais pas de contenu de retour

3XX : Redirection

Ici on est dans le cas où la requête a été réussie mais que l'on doit rediriger l'utilisateur. Globalement, on utilisera les codes 301 et 302.

  • 301 : Redirection permanente
  • 302 : Redirection temporaire

4XX : Requête invalide

Ici on est dans le cas où la requête est invalide. Globalement, on utilisera les codes 400, 401, 403 et 404.

  • 400 : Requête invalide
  • 401 : Non authentifié
  • 403 : Non autorisé
  • 404 : Non trouvé

5XX : Erreur serveur

Ici on est dans le cas où l'erreur est due au serveur. Globalement, on utilisera les codes 500, 502 et 503.

  • 500 : Erreur serveur
  • 502 : Mauvais port
  • 503 : Service indisponible

La sémantique des codes HTTP est très importante, et il faut la respecter. Ça permet à plein d'autres outils de fonctionner correctement, d'avoir des logs corrects, ainsi qu'à d'autres développeurs ou utilisateurs de l'API de comprendre ce qu'il se passe.

Schéma

Voici un schéma global d'exemple d'à quoi ressemble une API pour n'importe quelle entité, avec le statut HTTP correspondant.

  • GET /tasks -> 200
  • GET /tasks/:id -> 200
  • POST /tasks -> 201
  • PUT /tasks/:id -> 200
  • DELETE /tasks/:id -> 204

Il suffira de changer le nom de l'entité pour avoir une API pour n'importe quelle entité.

Sorting/Filtering

Ici on va utiliser une convention. Vous pouvez en utiliser d'autres, mais le plus important c'est de rester dans cette convention, rester cohérent.

  • Filtering: filter[field]=value (répétable)
  • Sorting: sort=field,field2 (séparé par des virgules)
  • Pagination: page=1 (page 1, offset pagination)

Exemple API

		import { Hono } from 'hono';
import { db } from '../db/clientDb.js';
import { task } from '../db/schema.js';
import { and, asc, desc, eq } from 'drizzle-orm';
 
export const app = new Hono();
 
// ....
 
const pageSize = 50;
app.get('/tasks', async (c) => {
	const url = new URL(c.req.url);
	const status = url.searchParams.get('filter[status]');
	const sortParam = url.searchParams.get('sort') ?? 'updatedAt';
	const page = Number(url.searchParams.get('page') ?? '1');
 
	// Map des colonnes triables pour rester typesafe
	const sortableColumns = {
		updatedAt: task.updatedAt,
		status: task.status
	};
 
	const orderByExpressions = sortParam.split(',').map((token) => {
		const isDesc = token.startsWith('-');
		const key = isDesc ? token.slice(1) : token;
		const column = sortableColumns[key] ?? task.order; // fallback sûr
		return isDesc ? desc(column) : asc(column);
	});
 
	const whereParts = [];
	if (status) whereParts.push(eq(task.status, status));
 
	const items = await db
		.select({
			id: task.id,
			title: task.title,
			description: task.description,
			status: task.status,
			updatedAt: task.updatedAt
		})
		.from(task)
		.where(whereParts.length ? and(...whereParts) : undefined)
		.orderBy(...orderByExpressions)
		.limit(pageSize)
		.offset((page - 1) * pageSize);
 
	return c.json({ data: items });
});