codelynx.dev
🇫🇷🇬🇧

Retour 20/03/2025

Next Safe Action mais pour les API Routes (NextJS)

Écris par Melvyn Malherbe le 20/03/2025


Tu cherches un outil qui permet de faire comme Next Safe Action mais pour les API Routes ? Ça existe !

Je te présente Next-Zod-Route une librairie qui te permet de transformer tes API Routes de ça :

TS
const BodySchema = z.object({
  name: z.string(),
});

const SearchParamsSchema = z.object({
  query: z.string(),
});

export const POST = async (
  req: Request,
  context: { searchParams: Promise<Record<string, string>> }
) => {
  const safeBody = BodySchema.safeParse(await req.json());
  const baseSearchParams = await context.searchParams;
  const safeSearchParams = SearchParamsSchema.safeParse(baseSearchParams);

  if (!safeBody.success) {
    return NextResponse.json({ error: safeBody.error.message }, { status: 400 });
  }

  if (!safeSearchParams.success) {
    return NextResponse.json(
      { error: safeSearchParams.error.message },
      { status: 400 }
    );
  }

  const body = safeBody.data;
  const searchParams = safeSearchParams.data;

  return NextResponse.json({ body, searchParams });
};

En ça :

TS
export const POST = route
  .body(
    z.object({
      name: z.string(),
    })
  )
  .query(
    z.object({
      q: z.string(),
    })
  )
  .handler(async (req, { body, query }) => {
    return NextResponse.json({ body, query });
  });

Beaucoup plus simple non ?

Et c'est pas tout car next-zod-route te permet d'ajouter :

  • des middlewares qui peuvent modifier la réponse
  • des validations pour le body, les params et les search params
  • retourner un simple object javascript au lieu d'un NextResponse
  • avoir des erreurs customisées

Validation des paramètres

Avec next-zod-route, tu peux facilement valider les paramètres de route, les paramètres de requête et le corps de la requête. Voyons comment ça fonctionne :

TS
const route = createZodRoute()
  .params(
    z.object({
      id: z.string().uuid(),
    })
  )
  .query(
    z.object({
      search: z.string().min(1),
    })
  )
  .body(
    z.object({
      field: z.string(),
    })
  )
  .handler(async (req, { params, query, body }) => {
    // params.id est correctement typé comme string
    // query.search est correctement typé comme string
    // body.field est correctement typé comme string

    return NextResponse.json({ params, query, body });
  });

Le typage est automatiquement inféré à partir de tes schémas Zod, ce qui rend ton code plus sûr et plus lisible.

Middlewares

Tu peux facilement ajouter des middlewares pour modifier ou valider les requêtes avant qu'elles n'atteignent ton handler :

TS
const authMiddleware: MiddlewareFunction = async ({ next, request }) => {
  // Code exécuté avant le handler
  const startTime = performance.now();

  // Transmission de données au contexte (ici tu peux récupérer le auth en fonction de la requête)
  const result = await next({ ctx: { user: { id: 'user-123', role: 'admin' } } });

  // Code exécuté après le handler
  const endTime = performance.now();
  console.log(`Request took ${Math.round(endTime - startTime)}ms`);

  return result;
};

export const GET = createZodRoute()
  .use(authMiddleware)
  .handler((request, context) => {
    // Accès aux données du middleware
    const { user } = context.ctx;
    return Response.json({ user });
  });

Les middlewares peuvent :

  • Exécuter du code avant et après le handler
  • Partager des données via le contexte
  • Court-circuiter la chaîne de middlewares (par exemple pour les vérifications d'authentification)
  • Modifier la réponse

Gestion des erreurs personnalisées

Tu peux personnaliser la gestion des erreurs pour traiter les cas spécifiques :

TS
class CustomError extends Error {
  statusCode: number;
  constructor(message: string, statusCode: number) {
    super(message);
    this.name = 'CustomError';
    this.statusCode = statusCode;
  }
}

const handleServerError = (error: Error) => {
  if (error instanceof CustomError) {
    return new Response(
      JSON.stringify({ message: error.name, details: error.message }),
      { status: error.statusCode }
    );
  }

  return new Response(JSON.stringify({ message: 'Something went wrong' }), {
    status: 500,
  });
};

export const GET = createZodRoute({
  handleServerError,
}).handler(() => {
  throw new CustomError('Not Found', 404);
});

Métadonnées avec validation

La fonctionnalité de métadonnées te permet d'associer des données validées à tes routes, particulièrement utile pour les vérifications d'autorisation :

TS
const permissionsMetadataSchema = z.object({
  permissions: z.array(z.string()).optional(),
});

const permissionMiddleware: MiddlewareFunction = async ({
  next,
  metadata,
  request,
}) => {
  const isAuthorized = hasPermission(request, metadata.permissions);

  if (!hasAllPermissions) {
    // Court-circuiter avec une réponse 403 Forbidden
    return new Response(
      JSON.stringify({
        error: 'Forbidden',
        message: 'You do not have the required permissions',
      }),
      {
        status: 403,
        headers: { 'Content-Type': 'application/json' },
      }
    );
  }

  // Continuer avec le contexte autorisé
  return next({ ctx: { authorized: true } });
};

export const GET = createZodRoute()
  .defineMetadata(permissionsMetadataSchema)
  .use(permissionMiddleware)
  // Statique
  .metadata({ requiredPermissions: ['read:users'] })
  .handler((request, context) => {
    const { authorized } = context.ctx;
    return Response.json({ success: true, authorized });
  });

Retour flexible

Tu peux retourner soit un objet Response standard, soit un simple objet JavaScript qui sera automatiquement converti en réponse JSON :

TS
// Retour direct d'un objet Response
export const GET = createZodRoute().handler(() => {
  return new Response(JSON.stringify({ custom: 'response' }), {
    status: 201,
    headers: { 'X-Custom-Header': 'test' },
  });
});

// Retour d'un objet JavaScript (converti automatiquement en JSON)
export const GET = createZodRoute().handler(() => {
  return { data: 'value' }; // Status 200 par défaut
});

Chaîne de middlewares avancée

Les middlewares peuvent travailler ensemble pour construire des fonctionnalités complexes :

TS
export const GET = createZodRoute()
  .use(async ({ next }) => {
    // Premier middleware
    const result = await next({ ctx: { user: { id: 'user-123' } } });
    return result;
  })
  .use(async ({ next, ctx }) => {
    // Deuxième middleware, accès au contexte du premier
    const user = ctx.user;

    const result = await next({ ctx: { permissions: ['read', 'write'] } });
    return result;
  })
  .handler((request, context) => {
    // Accès au contexte complet des deux middlewares
    const { user, permissions } = context.ctx;
    return Response.json({ user, permissions });
  });

Conclusion

next-zod-route est une solution complète pour gérer tes API Routes dans Next.js avec :

  • Validation des entrées (params, query, body) avec Zod
  • Système de middlewares flexible
  • Gestion personnalisée des erreurs
  • Métadonnées validées pour l'autorisation
  • Compatibilité avec différents formats de corps (JSON, form-data)

Si tu développes des API routes dans Next.js et que tu cherches une alternative à Next Safe Action, cette librairie est faite pour toi !

Pour l'installer :

BASH
npm install next-zod-route
# ou
yarn add next-zod-route
# ou
pnpm add next-zod-route

Retrouve le code source et la documentation complète sur GitHub.