Generar automáticamente un cliente de API de Next.js seguro desde NestJS Swagger
Aprenda cómo eliminar los tipos API duplicados utilizando NestJS Swagger y Orval para generar automáticamente ganchos React Query seguros desde el punto de vista tipológico para Next.js.
La creación de una aplicación full-stack en TypeScript suele comenzar con mucho esfuerzo repetitivo.
Definís un tipo de solicitud en el backend de NestJS. Luego redefinís esa misma estructura en el frontend de Next.js. Construís un punto de extremo del controlador y luego implementáis manualmente una llamada fetch para acceder a él. Modificáis la respuesta de la API y luego esperáis haber recordado todos los lugares en el cliente que dependen de ella.
Esto funciona bien al principio.
Pero a medida que la superficie de la API se expande, los tipos duplicados y la lógica de solicitud escrita a mano se convierten en una fuente constante de errores y pérdida de tiempo.
Un enfoque más sostenible es tratar el contrato de API del backend como la única fuente de verdad.
Este flujo de trabajo se basa en:
- NestJS
- Swagger
- Orval
- Next.js
- TanStack Query
La idea central es sencilla:
NestJS endpoints + Swagger DTOs
→ OpenAPI document
→ Orval generation
→ TypeScript types, request functions, and React Query hooks
→ Next.js frontend
En lugar de sincronizar manualmente los tipos del frontend y el backend, se regenera directamente el cliente a partir del contrato de la API cada vez que este cambia.
El proyecto completo utilizado como ejemplo está disponible en este repositorio: next-modern-stack en GitHub.
El problema: los tipos de la API se desalinean
Imagínese añadiendo una función de “crear nota” a una aplicación de bloc de notas con estilo terminal.
Una implementación típica escrita a mano en el frontend podría verse más o menos así:
type CreateNoteInput = {
text: string;
folderId: number;
};
type Note = {
id: number;
text: string;
folderId: number;
createdAt: string;
};export async function createNote(input: CreateNoteInput): Promise<Note> {
const response = await fetch("http://localhost:3001/notes", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}); if (!response.ok) {
throw new Error("Could not create note");
} return response.json();
}
Ninguno de estos códigos está intrínsecamente incorrecto.
El problema es que ahora debes encargarte manualmente de mantener todo un conjunto de elementos: la estructura de la solicitud saliente, la estructura de la respuesta entrante, qué URL llamar, qué verbo HTTP utilizar, cómo se muestran los errores, cómo se sigue el estado de carga, cómo se representa el estado de la mutación y cómo se manejan el caché y las solicitudes de actualización.
Ahora supongamos que el backend cambia.
Tal vez folderId sea renombrado. Tal vez la respuesta adquiera un nuevo campo. Tal vez cambie la ruta. Tal vez la API comience a devolver una estructura completamente diferente.
Tu frontend puede desincronizarse sin ninguna advertencia.
La solución es dejar de tratar al frontend y al backend como dos fuentes independientes de verdad.
Hacer que Swagger sea el contrato de la API
Swagger permite que tu API NestJS describa sus propios puntos finales, cuerpos de solicitud y modelos de respuesta.
A partir de esos metadatos, NestJS puede generar un documento OpenAPI completo.
Aquí hay un DTO para crear una nota:
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
@ApiSchema({ name: "CreateNote" })
export class CreateNoteDto {
@ApiProperty({
description: "The text content of the note",
})
text: string; @ApiProperty({
description: "The ID of the folder this note belongs to",
})
folderId: number;
}
Esto define con exactitud qué forma debe tener el cuerpo de la solicitud.
A continuación, se documenta el propio endpoint:
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateNoteDto } from "./create-note.dto";
import { NoteDto } from "./note.dto";
import { NotesService } from "./notes.service";
@Controller("notes")
export class NotesController {
constructor(private readonly notesService: NotesService) {} @Post()
@ApiOperation({
summary: "Create a note",
operationId: "createNote",
})
@ApiResponse({
status: 201,
description: "The note has been successfully created.",
type: NoteDto,
})
create(@Body() createNoteDto: CreateNoteDto) {
return this.notesService.create(
createNoteDto.text,
createNoteDto.folderId,
);
}
}
Hay dos detalles importantes aquí:
CreateNoteDtodefine el cuerpo de solicitud esperado.NoteDtodefine la estructura de una respuesta exitosa.
El campo operationId también desempeña un papel clave.
operationId: "createNote";
Asigna al endpoint un nombre estable y legible por humanos dentro del cliente generado.
Eso permite que el frontend llame posteriormente a un gancho con ese nombre:
useCreateNote();
en lugar de algo vago o derivado automáticamente del camino de la ruta original.
Publicar documentación Swagger desde NestJS
Una vez que sus controladores y DTOs estén anotados, el siguiente paso es conectar Swagger cuando la aplicación NestJS se inicie.
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule); app.enableCors({
origin: "http://localhost:3000",
}); const config = new DocumentBuilder()
.setTitle("Next Modern Stack API")
.setDescription("API documentation for Next Modern Stack")
.setVersion("1.0")
.build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup("api-docs", app, document); await app.listen(process.env.PORT ?? 3001);
}bootstrap();
Una vez que su API esté en ejecución localmente, Swagger expone dos puntos de acceso importantes:
http://localhost:3001/api-docs
http://localhost:3001/api-docs-json
La primera URL proporciona la interfaz interactiva de Swagger UI, donde puede navegar y probar los puntos de acceso manualmente.
La segunda devuelve el documento OpenAPI en formato JSON, y esto es exactamente lo que Orval utiliza para crear su cliente frontend.
Generar el cliente API de Next.js con Orval
La función de Orval es leer ese documento OpenAPI y convertirlo en código TypeScript que su aplicación Next.js pueda importar directamente.
En esta configuración, el archivo de configuración de Orval se encuentra dentro del propio proyecto Next.js:
import { defineConfig } from "orval";
export default defineConfig({
api: {
input: "http://localhost:3001/api-docs-json",
output: {
target: "./src/generated/api.ts",
client: "react-query",
httpClient: "fetch",
baseUrl: "http://localhost:3001",
},
},
});
Esta configuración indica a Orval que:
- obtener el JSON de Swagger desde el servidor NestJS en ejecución
- escribir el cliente generado en
src/generated/api.ts - crear los ganchos de TanStack Query junto con las funciones en bruto
- utilizar la API nativa del navegador
fetchpara las solicitudes - dirigir esas solicitudes a la instancia local de NestJS
El paquete Next.js define un script para activar la generación:
{
"scripts": {
"generate": "orval --config orval.config.ts"
}
}
En la raíz del monorepo, Turborepo distribuye esa orden a cada espacio de trabajo que la necesite:
{
"scripts": {
"generate": "turbo run generate"
}
}
Desde la raíz del repositorio, una sola orden regenera todo:
bun run generate
Tenga en cuenta que su servidor NestJS debe estar en ejecución previamente, ya que Orval obtiene su esquema de:
http://localhost:3001/api-docs-json
Qué genera Orval
Al ejecutar el generador se crea un archivo similar a este:
apps/web/src/generated/api.ts
Trate este archivo como salida de compilación, no como código fuente; no lo edite manualmente.
Si es necesario hacer algún cambio, actualice los DTOs del backend y las anotaciones de Swagger, y luego ejecute nuevamente la generación para volver a crear el cliente.
Con un contrato de API bien definido, Orval puede generar:
- Tipos de TypeScript tanto para solicitudes como para respuestas
- Funciones de solicitud completamente tipadas
- Ganchos TanStack Query para obtener datos
- Ganchos TanStack Query para realizar mutaciones
- Funciones auxiliares que exponen claves de consulta para invalidar el caché
Por ejemplo, el endpoint de carpetas está anotado con este ID de operación:
@ApiOperation({
summary: "Get all folders",
operationId: "getFolders",
})
Orval convierte eso en un gancho listo para usar en el frontend:
useGetFolders();
junto con una función auxiliar de clave de consulta correspondiente:
getGetFoldersQueryKey();
Dado que el ID de la operación se define explícitamente en el backend, los nombres de los ganchos y auxiliares generados permanecen consistentes y predecibles, en lugar de deducirse del camino de la URL.
Uso de ganchos generados en Next.js
Con el cliente generado por Orval, su frontend de Next.js ya no necesita una llamada fetch escrita a mano para cada endpoint.
Considere el patrón utilizado en una función de bloc de notas en la terminal:
import { useQueryClient } from "@tanstack/react-query";
import {
getGetFoldersQueryKey,
useCreateNote,
useGetFolders,
} from "@/generated/api";
export function TerminalContent() {
const queryClient = useQueryClient(); const { data: foldersData } = useGetFolders(); const { mutateAsync: createNote } = useCreateNote(); async function handleCreateNote(text: string, folderId: number) {
await createNote({
data: {
text,
folderId,
},
}); await queryClient.invalidateQueries({
queryKey: getGetFoldersQueryKey(),
});
} return null;
}
El flujo funciona de la siguiente manera:
useGetFolders()recupera la lista de carpetas actual.useCreateNote()envía la solicitud para crear una nota.- Una vez que la mutación se resuelve con éxito,
getGetFoldersQueryKey()apunta a la entrada de caché que necesita actualizarse,- y TanStack Query vuelve a cargar automáticamente los datos de las carpetas.
Como resultado, la interfaz refleja el estado más reciente del servidor sin que sea necesario sincronizar manualmente las partes anidadas del estado de React. Este es uno de los mayores beneficios de combinar los hooks generados con la gestión de caché de TanStack Query.
Usar datos iniciales cuando el servidor ya los tiene
En muchas configuraciones de Next.js, algunos datos ya están disponibles en el servidor antes incluso de que se monte un componente del cliente.
Por ejemplo, el componente terminal podría recibir sus carpetas como propiedades y pasarlas al hook como datos iniciales:
const { data: foldersData } = useGetFolders({
query: {
initialData: {
data: initialFolders,
status: 200,
headers: new Headers(),
},
},
});
Al hacer esto, la página se renderiza al instante utilizando los datos ya obtenidos del lado del servidor, mientras TanStack Query se encarga de la caché y de cualquier recarga posterior. Se conservan las ventajas de la capa de obtención de datos generada sin descartar el trabajo que Next.js ya realizó por usted.
El flujo de trabajo cuando cambia tu API
Cada vez que agregue o modifique un endpoint, siga esta secuencia:
1. Update the NestJS controller or service
2. Update Swagger DTOs and endpoint metadata
3. Start the API locally
4. Run bun run generate
5. Review the generated API client changes
6. Update frontend usage where needed
7. Run bun run lint:fix
8. Let TypeScript show you any remaining mismatches
Por ejemplo, supongamos que la carga útil para crear una nota cambia de esta forma:
{
text: string;
folderId: number;
}
a esta otra, con una bandera adicional:
{
text: string;
folderId: number;
isPinned: boolean;
}
Deberá actualizar el DTO del backend en consecuencia:
@ApiSchema({ name: "CreateNote" })
export class CreateNoteDto {
@ApiProperty()
text: string;
@ApiProperty()
folderId: number; @ApiProperty()
isPinned: boolean;
}
Luego, genere nuevamente el cliente:
bun run generate
A partir de ese momento, al llamar a createNote() en el frontend se requiere isPinned, y TypeScript marcará cada lugar de llamada que aún necesite actualización. Ese tipo de retroalimentación inmediata impulsada por el compilador es mucho más fiable que confiar en recordar uno mismo todos los lugares donde un tipo mantenido manualmente necesita ajustes en una base de código separada.
Por qué esto es mejor que un paquete de tipos compartidos
Un patrón común en monorepos es crear un paquete dedicado, algo como:
packages/
└── types/
Tanto el frontend como el backend importan entonces las mismas interfaces de TypeScript desde esa ubicación compartida.
Esto puede funcionar razonablemente bien en ciertas situaciones.
No obstante, solo aborda parte del desafío de mantener una API sincronizada.
El simple hecho de compartir interfaces deja varias cosas sin resolver:
- endpoints descritos en la documentación
- funciones de solicitud que incluyan tipos adecuados
- ganchos de mutación que incluyan tipos adecuados
- claves de caché consistentes
- rutas de endpoints centralizadas
- definiciones de métodos HTTP consistentes
- una referencia que otros desarrolladores puedan consultar
- un contrato que otros clientes puedan utilizar
En cambio, la combinación de Swagger y Orval ofrece un pipeline centrado en la API.
El backend define y es el responsable del contrato.
El frontend simplemente consume el código generado a partir de ese contrato.
El resultado es una separación mucho más clara entre las dos aplicaciones.
Errores comunes que deben evitarse
Editar manualmente los archivos generados
Nunca edite directamente un archivo como este:
apps/web/src/generated/api.ts
Cualquier cambio que haga allí se borrará la próxima vez que se genere el cliente.
En su lugar, corrija el contrato en el backend y genere nuevamente el cliente.
Omitir operationId
Si deja los IDs de operación sin definir, los nombres de las rutas en el código generado pueden resultar desordenados o impredecibles.
Asigne en su lugar IDs claros y descriptivos, como por ejemplo:
operationId: "getFolders";
operationId: "createNote";
operationId: "updateNote";
Al hacerlo, los nombres de los ganchos en el frontend se vuelven mucho más fáciles de leer.
Olvidar generar nuevamente después de cambios en el backend
El frontend no tiene forma de saber que un endpoint ha cambiado hasta que se vuelve a ejecutar el paso de generación.
Considere la regeneración como una parte rutinaria de su ciclo de desarrollo, y no algo que se hace al final.
Escribir funciones fetch personalizadas junto a los hooks generados
Utilice por defecto los hooks que Orval genera para usted.
Solo recurra a una función fetch escrita a mano cuando se enfrente a una limitación real que el cliente generado no pueda manejar.
De lo contrario, solo estará reintroduciendo la misma lógica de solicitud duplicada que intentaba eliminar.
Tratar los tipos generados como validación en tiempo de ejecución
Los tipos de TypeScript generados son útiles para detectar errores mientras escribe código.
No brindan protección contra entradas desconocidas o mal formadas que lleguen en tiempo de ejecución.
Para cosas como envíos de formularios, parámetros de URL, cargas de webhook o datos de servicios de terceros, combine sus tipos con una validación en tiempo de ejecución real, utilizando herramientas como Zod.
Un contrato API, menos trabajo repetitivo
La mayor ventaja de combinar Swagger y Orval no es solo una mayor seguridad en los tipos.
Es que deja de ser necesario tomar las mismas decisiones una y otra vez.
En lugar de reconstruir manualmente una capa API para cada endpoint, describe el contrato una sola vez y permite que las partes repetitivas y predecibles se generen automáticamente.
NestJS endpoint
→ Swagger contract
→ Orval generated client
→ TanStack Query hook
→ Next.js UI
Las ventajas incluyen:
- ménos definiciones de tipos duplicadas
- ménos funciones de solicitud escritas a mano
- un límite más claro entre frontend y backend
- errores inmediatos en TypeScript cuando cambia la estructura de la API
- ganchos para consultas y mutaciones ya preparados
Esta configuración también hace que sea más seguro permitir que las herramientas de IA contribuyan al código base.
Cuando un asistente de IA agrega un nuevo punto final del backend, puedes dirigirlo a través de una secuencia sencilla:
Update the NestJS controller and DTOs
→ document the endpoint with Swagger
→ run bun run generate
→ use the generated hook in Next.js
→ run Biome
Ese es un patrón mucho más fiable que pedirle a un asistente de IA que invente y mantenga código de API disperso y duplicado en todo el proyecto.
Construye el flujo de trabajo completo
Este pipeline de Swagger y Orval es solo una parte de una configuración moderna más amplia en TypeScript que combina Next.js y NestJS, utilizando herramientas como espacios de trabajo Bun, Turborepo, PostgreSQL con Prisma, TanStack Query, nuqs, Biome y Lefthook, además de flujos de trabajo asistidos por IA basados en reglas y habilidades reutilizables.
Puedes inspeccionar un ejemplo completo y funcional de esta configuración aquí:
Lecturas relacionadas
- Creación de interfaces de agente de IA de múltiples pasos con Next.js y el AI SDK — Aprenda cómo diseñar una interfaz de agente de IA lista para producción utilizando herramientas tipadas, bucles de múltiples pasos y componentes de UI generativos en tiempo real en Next.js.
- Convierte los controladores de rutas de Next.js en una capa BFF deliberada — Aprenda qué soluciona el patrón Backend for Frontend, por qué vuelve a utilizarse en aplicaciones Next.js y cómo evitar que los controladores de rutas se conviertan en objetos excesivamente complejos.