Автаматычна генераванне безпечнага кліента API Next.js на адказе NestJS Swagger
Выявіце, як пазбутыся ад дуплікацый типаў API за дапамой NestJS Swagger і Orval, каб аўтаматычна ствараць безпечныя з точкі зору типаў хукі React Query для Next.js.
Стварэнне аплікацыі на TypeScript з падтрымкам усіх слоёў зазвычай пачынаецца з вельмі большой колькасці павтараючыхся зусиль.
Вы задаеце тип запиту на бэкендзе NestJS. Потым вы занова задаеце той самы формат на фронтендзе Next.js. Вы ствараеце канцэнтры прыемніка, а потым ручна выкананае запусканне fetch-запытку для доступу да яго. Вы правяце адпаведны адпаведзь API, а потым спадзяваецеся, што памятаеце пра кожны месца на кліентзе, які ад нья залежыць.
Этае падходжэнне працуе добра на першых этапах.
Але калі прастор API расширваецца, дублюючыяся типы і ручна напісаная логіка запытоў стаюць стойкым джэрелам багоў і втрачанага часу.
Болей стойкі падход — адносіцца да контракта API бэкенда як да едынага адпаведныя джэрела.
Эты працоўны процес выкорыстоўвае:
- NestJS
- Swagger
- Orval
- Next.js
- TanStack Query
Основная ідея проста:
NestJS endpoints + Swagger DTOs
→ OpenAPI document
→ Orval generation
→ TypeScript types, request functions, and React Query hooks
→ Next.js frontend
У замест на ручную сінхронізацыю типаў фронтэнду і бэкэнду, вы перагенераваеце кліента безпасова з кантракта API кожны раз, калі ён зміняецца.
Полны проект, які выкарыстоўваецца як прыклад, доступны ў гэтым репазітарыі: next-modern-stack на GitHub.
Проблема: типы API адступаюць адзін ад другога
Уявіце сабе, што вы дадаеце функцыю „стварыць значку“ да прыкладніка-зошыту у стыле тэрмінала.
Тыповая ручная реалізацыя фронтэнду можа выглядаць прыблізна так:
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();
}
Нічыя з гэтых частак коду не ў сваёй сутнасі няправильныя.
Проблема заключаецца ў тым, што тепер вам трэба самостайна караць цэлы набор аспектаў: формат выходнага запиту, формат прыходзячай адпаведзі, URL, які трэба вызваць, HTTP-дзеянне, якое трэба выкарыстоўваць, спосаб паказу прычын адказоў, спосаб стежэння за статусам завантажэння, спосаб адображэння статусу мутацыі, а таксама спосаб кэшавання і падтрымкі паўторнага запыту.
Тепер заўважкім, што змянюецца бэкенд.
Можа, folderId будзе перайменаваны. Можа, у адпаведзі з’явіцца новая польна. Можа, шлях маршруту змяніцца. Можа, API пачне вяртаць абсалютна іншы формат дадзеных.
Ваш фронтэнд можа выйсці за межы сінхронізаціі без жадных паведамленняў.
Рашэнне — перестаць спрыягаць фронтэнд і бэкенд як два незалежныя джерелы правды.
Зробіце Swagger даговорам API
Swagger дазволяе вашаму NestJS API описваць своія канцэнтры, тэлы запытаў і моделі адпаведзей.
За дапамою гэтых метаданых NestJS можа стварыць цэлы дакумент OpenAPI.
Ёсць DTO для стварэння прымечання:
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;
}
Гэта чытча визначае, якія параметры павінны быць у тэле запита.
Далей трэба задокументаваць сам пункт прыёму:
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,
);
}
}
Два аспекты тут маюць вялікое значэнне:
CreateNoteDtoвизначае апэктивнае тэлу запита.NoteDtoвизначае формат успешнай адпаведзі.
Поле operationId таксама выплывае ключовую ролю.
operationId: "createNote";
У ствароўваным кліенте ён прызначае пункту прыёму стабільнае, чытальнае для людзей імя.
Гэта дазволяе фронтэнду пазней вызваць хук пад такім іменем:
useCreateNote();
замест чаго-небудзь нечыткага або автоматычна выведанага з первіснага шляху маршрута.
Публікацыя дакументацыі Swagger з NestJS
Калі вашыя кантролеры і DTO-ы аннотаваны, наступны крок — падключэнне Swagger колі запускаецца аплікэцыя NestJS.
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();
Калі ваш API запускаецца локальна, Swagger адкрывае два канчыкі, якія варта знать:
http://localhost:3001/api-docs
http://localhost:3001/api-docs-json
Першы URL аддае інтэрактыўны інтерфейс Swagger UI, дзе можна пераглядаць та ручна тэставаць канчыкі.
Другі вяртае суровы дакумент OpenAPI у формате JSON, і самэ гэта Orval выкарыстоўвае для стварэння кліента фронтэнда.
Стварэнне кліента API Next.js з Orval
Задача Orval — прыняць гэты дакумент OpenAPI і ператворыць яго на код на TypeScript, які ваша аплікэцыя Next.js можа безпасабліва імпортаваць.
У такой наладзе файл конфігурацыі Orval знаходзіцца ў самай проектацыі 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",
},
},
});
Гэтая наладка паведамляе Orval пра тое, калі ён должен дзейсніць певныя дзеянні:
- З’явіць Swagger JSON з працюючага сервера NestJS
- Запішыце створаны кліент у
src/generated/api.ts - Створыце хукі TanStack Query разам з первіснымі функцыямі
- Выкорыстоўваце вбудованую API прашэнняў браузера
fetchдля з’явоў - Направіце гэтыя з’явы на локальную інстанцую NestJS
Пакет Next.js задае скрыпт для запуску процесу генеравання:
{
"scripts": {
"generate": "orval --config orval.config.ts"
}
}
У корэні монорепо Turborepo розпрашвае гэтыя команды ў кожным рабочым просторе, які іх патрабуе:
{
"scripts": {
"generate": "turbo run generate"
}
}
З корэню репазітарыя адна команда перагенеруе всё:
bun run generate
Памятайце, што ваш сервер NestJS павінен быць запущаны раней, адколі Orval з’являе сваю схему з:
http://localhost:3001/api-docs-json
Што генеруе Orval
Запуск генератора стварае файл, падобны да гэтаго:
apps/web/src/generated/api.ts
Спрытваце гэты файл як рэзультат будовы, а не кансольны код — не рэдагаваце яго вручную.
Якщо чыгуць якія-небудзь змены, апдэйтуйце DTO-ы на бэкенде і анотаціі Swagger, пасля чаго зноў запускайце процес генеравання, каб парадзіць кліента занова.
Пры наявнасці чытко адзначанага контракта API, Orval можа выдаць:
- Тыпы TypeScript для запитоў і адказаў
- Функцыі запитоў з полным типаваннем
- Хукі TanStack Query для запошчання дадзэнняў
- Хукі TanStack Query для мутацый
- Памагальныя функцыі, якія адкрываюць ключы запитоў для банкротавання кэша
Як прыклад, тапуск папакі анотаваны гэтым ID операцыі:
@ApiOperation({
summary: "Get all folders",
operationId: "getFolders",
})
Orval ператварае гэта на хук, які можна вжываць на фронтэнде:
useGetFolders();
разам з памагальной функцыяй для ключа запиту:
getGetFoldersQueryKey();
Паколькі ідэнтыфікатор аператыў ясна задаецца на бэкендзе, імены ствароўваных хукіў і памагальных функцый застаюцца адносовымі і прыведамымі, замест таго каб іх дагадвацца на аднойчыні з шляхам URL.
Іспользаванне ствароўваных хукіў у Next.js
За дапамогою кліента, стваранага Orval, фронтэнд Next.js больш не патрабуе ручнага вызову fetch для кожнага эндпункта.
Разглянемце шаблон, які выкарыстоўваецца у функцыяй з блокнотам у тэрмінале:
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;
}
Алгорытм працюе так:
useGetFolders()атрымлівае спіс текущых папак.useCreateNote()адправляе запыт на стварэнне зьведамлення.- Калі мутацыя успешна завершыцца,
getGetFoldersQueryKey()паказывае ентры в кэшы, якія трэба апдэйтаваць,- і TanStack Query автаматычна паўтарна атрымлівае данні папак.
У результате інтэрфейс практычна адражае найновейшы стан сервера, без неабяжнага ручнага сінхронавання вкладзеных элементаў стану React. Гэта адна з найбольшых пераваг выкарыстоўвання ствароўных хуків разам з адмэнстрацыяй кеша TanStack Query.
Выкарыстоўванне пачатковых дадзенняў, калі сервер іх вялікі
У багатых настаўленнях Next.js частыя дадзеныя вялікі на серверы ўжо да таго, як кліентскі компонент будзе запускацца.
Напрыклад, компонент тэрміналу можа прымаць свае папкі як параметры і перадаваць іх у хук як пачатковыя дадзеныя:
const { data: foldersData } = useGetFolders({
query: {
initialData: {
data: initialFolders,
status: 200,
headers: new Headers(),
},
},
});
Такой падход дазволяе сторунцу мгновенна адразавацца за дапамой дадзенняў, якія вже былі запрашаны на серверы, тады калі TanStack Query продовжае адпавядаць за кэшаванне і празбіранне дадзенняў пазней. Вы застаецеся з перавагамі шару запрашання дадзенняў, створанага автаматычна, не відмовляючыся ад роботы, яку Next.js вялікі для вас.
Процес работы, калі змянюецца ваш API
Калі бярэце адно з канчыкаў сеткі чыстаць або яго мяніце, дазвольце пашледзець гэтыя крокі:
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
Як прыклад, заўважкім, што пакет дадзеных для стварэння запіску зменяецца з такога віда:
{
text: string;
folderId: number;
}
на такі, дзе дадаўся ўсіленнік:
{
text: string;
folderId: number;
isPinned: boolean;
}
Тады трэба будзе адпаведна апцэнзіраваць DTO-код у бэкенде:
@ApiSchema({ name: "CreateNote" })
export class CreateNoteDto {
@ApiProperty()
text: string;
@ApiProperty()
folderId: number; @ApiProperty()
isPinned: boolean;
}
Пасля чаго трэба перзапрацаваць кліентскую частку:
bun run generate
З таго моменту вызыванне метода createNote() у фронтэнде будзе вымагаць наявнасці атрыбута isPinned, і TypeScript пакажае кожны месца вызыву, якія ўсё яшчэ трэба апцэнзіраваць. Такая негайная, кампайляром керованая адзвечнае ўстаўка є набліжэй надзейная, чым пакладанне на сабе памяці пра кожны месца, дзе ручна адтульнаваны тип трэба паспрыяць у адной з незалежных баз коду.
Чаму гэта краща, чым пакет зяўніх типоў
Звычны патэрн у манорепох ёсць стварэнне спецыяльнага пакета, на прыклад:
packages/
└── types/
Фронтэнд і бэкэнд абы два імпортуюць тыя ж інтерфейсы TypeScript з гэтага спакульнаранага месца.
Гэта можа працаваць досыць добра ў певных ситуациях.
Аднак гэта рашае толькі частку проблемы падтрымання синхроннасці API.
Простае спакульнаванне інтерфейсоў заставляе безліч рэчаў застацца без уваги:
- тэрміналы, описаныя ў дасведчэннях
- функцыі запитоў, якія несу правильныя типы
- хукі мутацый, якія несу правильныя типы
- едначынныя ключы кэша
- централізаваныя шляхі тэрміналоў
- едначынныя апісанні методаў HTTP
- рэферэнс, які іншыя разработчыкі могу пераглядаць
- контракт, які іншыя кліенты могу выкарыстоўваць
Узаемадзейнаўанне Swagger і Orval дае вам, замест таго, парадокс API-першыя процесы.
Бэкэнд апісвае і ёст кантракт.
Фронтэнд проста ўжывае код, створаны з таго кантракту.
Рэзультатам яе ёсць набліжэнне двух прыкладоў праграмаў.
Пашытковыя памылкі, якіх трэба ужыважваць
Ручная рэдагаванне створаных файлаў
Ніколі не рэдагавайце такі файл безпасяродна:
apps/web/src/generated/api.ts
Усі ваши змяны будуць стараненыя наступны раз, калі кліент будзе пераствораны.
У замене выправіце кантракт у бэкэндзе і перастворыце кліент.
Ігнараванне operationId
Якщо вы не зададзіце ідэнтыфікаторы аперацый, назвы маршрутаў у створанам кодзе можу стаць неразбірлівымі або непрадказуемымі.
У замене задаце чыстыя, апісавальныя ідэнтыфікаторы, напрыклад:
operationId: "getFolders";
operationId: "createNote";
operationId: "updateNote";
Такое рашэнне дае набліжэнныя назвы хукав у фронтэндзе.
Забыванне перастварыць пасля змян у бэкэндзе
Фронтэнд не можа знайсці, што змяніўся канечны пункт, пакуль вы знова не запусціце крок генеравання.
Спрыяйце регенерацыі як часткі вашага циклу развіцця, а не як чамусу, што робится пасля іншых дзеянняў.
Стварэнне спецыяльных функцый fetch палягліва на генераваных хукі
За замовчаннем вжывайце хукі, якія Orval генеруе для вас.
Зверніцеся да ручна напісанай функцыі fetch толькі тады, калі ступіце на справжню працоўную пераканальність, якую генераваны кліент не можа адрабатваць.
Інакш вы проста зноў вводзіце тую ж самую логіку запитоў, якую намагаліся адмахнуцца.
Вывядзенне генераваных типаў як пераканальніцтва пад час выконання
Генераваныя типы TypeScript ўжытковы для выяўлення памылак пад час напісання коду.
Яны не забезпечваюць захоплення ад нявядомых чы рашыдзеных дадзенаў, якія прайшлі пад час выконання.
Для такіх речаў, як адправка форм, параметры URL, пакеты дадзеных з webhook-аў чыста як і дадзеныя з сервісаў трэціх сторон, неабходна супарабатаваць вашы типы з рэальной перакананнем пад час выконання, выкарыстоўваючы такія інструменты, як Zod.
Адна даговорнае супакоўка API, менш павтаральнай роботы
Найбольшая перадчыннасць ад спаўнення Swagger і Orval — гэта не толькі павышаная безпека типаў.
Гэта тое, што вам больш не трэба зноў і зноў прымець тыя ж рашэнні.
Уместа таго, каб вручную перастраіваць шар API фронтэнду для кожнага эндпоінта, вы описваеце даговорнае супакоўка ўсёродзе і дазволяеце автаматычна генераваць тыя павтаральныя, прагнозаваныя часткі.
NestJS endpoint
→ Swagger contract
→ Orval generated client
→ TanStack Query hook
→ Next.js UI
Прычыны для цього включаюць:
- менш копіяў абмовленняў типаў
- менш функцый запытанняў, напісаных вручную
- яснейшыя межы між фронтэндам і бэкендам
- негайныя паказанні каштоўкаў TypeScript, калі змянюецца форма API
- гатовыя хукі для запытанняў і мутацый
Такая наладка таксама робіць болей безпечным дазволенне інструментам AI дапамагаць у стварэнні кодавой базы.
Калі AI-асистэнт дадае новы канцэнтр паказчика, вы можете направіць його через простую последовасць:
Update the NestJS controller and DTOs
→ document the endpoint with Swagger
→ run bun run generate
→ use the generated hook in Next.js
→ run Biome
Это набліжнае надзейнейша схема, чым прасіць AI-асистэнта ствараць і падтрымліваць розрознены, дуплікаваны код API по всім проектам.
Стварэнне цэлага рабочага процесу
Этая пайплайн-схема з Swagger і Orval — частка шырэйшай сучаснай наладкі на TypeScript, якая спаўнае Next.js і NestJS, выкарыстоўваючы такія інструменты, як працоўныя прасторы Bun, Turborepo, PostgreSQL з Prisma, TanStack Query, nuqs, Biome і Lefthook, а таксама рабочыя процесы з дапамогай AI, пабудаваныя на перадаўальных правілах і навыках.
Вы можете пераглянуць цэлы, працуючы прыклад такой наладкі тут:
Супаўязаная літэратура
- Стварэнне UI для багатаэтапных AI-агентаў з Next.js і AI SDK — Дазвольце вам дазнацца, як спроектаваць інтерфейс AI-агента, гатовы да выкарыстання ў працэсе роботы, з викорыстанням інструментаў з типаваннем, багатаэтапных цыклаў і компонентаў UI, якія генеруюць данні пад час роботы, у Next.js.
- Ператварэнне працоўнікаў маршрутаў Next.js у спецыяльны слой BFF — Дазвольце вам дазнацца, што рашае патэрн Backend for Frontend, чаму ён знова выкорыстоўваецца у дапрацоўках на Next.js і як утримацца ад ператварэння працоўнікаў маршрутаў у занадто сложныя структуры.