Przejdź do głównej zawartości

Rozwój API z AI

Twój resolver GET /posts ładuje autora każdego wpisu osobnym zapytaniem. Przeszedł przez review i demo, bo demo miało trzy wpisy. Na produkcji feed renderuje 50 wpisów, resolver odpala 51 zapytań, a pula połączeń do bazy płonie już o 9 rano. AI napisało dokładnie to, o co prosiłeś — po prostu nie znało twojego wzorca dostępu, bo prompt nigdy mu go nie podał.

AI jest naprawdę szybkie w pracy z API: spec-to-code, walidacja, middleware błędów, testy kontraktowe. Ale “szybko” zamienia się w “płonie na produkcji”, gdy pozwolisz mu improwizować kształt systemu. Niezawodny przepływ pracy to spec-first: przypnij kontrakt (OpenAPI, schemat GraphQL lub proto), każ AI generować względem niego, a o tym, kiedy jest gotowe, niech zdecydują twoje testy — nie demo.

  • Pętlę spec-first, w której kontrakt napędza generowanie, więc implementacja nie może po cichu odpłynąć
  • Gotowe do wklejenia prompty, które nazywają stack (Express + TypeScript + Zod + Vitest, Pact do kontraktów, k6 do obciążenia), zamiast zostawiać nawiasy [placeholder]
  • Wariant dla Cursor / Claude Code / Codex dla spec-to-code, przebiegów kontraktowych w CI i regeneracji SDK
  • Tryby awaryjne, które gryzą API generowane przez AI: rozjazd specyfikacji, resolvery N+1, brakujące kursory paginacji, kolejność middleware autoryzacji
  1. Przypnij kontrakt. Najpierw wygeneruj specyfikację OpenAPI, schemat GraphQL lub plik proto i przejrzyj go jako człowiek. To artefakt, względem którego sprawdzane jest wszystko inne.

  2. Generuj względem kontraktu. Wskaż agentowi plik specyfikacji i poproś o zaimplementowanie endpointów/resolverów z walidacją i obsługą błędów — nie o wymyślanie API na bieżąco.

  3. Zablokuj zachowanie testami. Wygeneruj testy jednostkowe, integracyjne i kontraktowe. Spraw, by kształty odpowiedzi w testach dokładnie pasowały do handlerów, a potem uruchom je w CI.

  4. Zregeneruj klientów. Ponownie uruchom generator SDK z (już autorytatywnej) specyfikacji, by konsumenci pozostali zsynchronizowani z serwerem.

Niezależnie od protokołu, najpierw skłoń AI do wyprodukowania kontraktu, zanim powstanie jakakolwiek implementacja. Bądź konkretny co do dojrzałości i konwencji, których oczekujesz.

AI zwraca specyfikację, którą możesz przejrzeć i wersjonować. Przycięty wycinek tego, czego się spodziewać:

paths:
/tasks:
get:
summary: List tasks
parameters:
- { name: status, in: query, schema: { type: string, enum: [todo, in_progress, done] } }
- { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
- { name: cursor, in: query, schema: { type: string } }
responses:
'200':
description: Paginated task list
content:
application/json:
schema: { $ref: '#/components/schemas/TaskList' }

Dla GraphQL poproś o schemat z rozpisanymi typami connection i subskrypcjami; dla gRPC poproś o .proto ze strumieniowymi RPC i maskami pól. Dyscyplina jest ta sama: najpierw kontrakt, review, potem implementacja.

Teraz wskaż agentowi specyfikację i nazwij stack. Kształt odpowiedzi, który zwróci, musi pasować do tego, co będą asertować twoje testy — rozjazd w tym miejscu jest źródłem numer jeden problemu “przechodzi lokalnie, zwraca 500 w CI”.

Reprezentatywny wycinek tego, co produkuje agent:

import { Router } from 'express';
import { z } from 'zod';
import { requireAuth } from '../middleware/auth';
const listQuery = z.object({
status: z.enum(['todo', 'in_progress', 'done']).optional(),
limit: z.coerce.number().int().min(1).max(100).default(20),
cursor: z.string().optional(),
});
const router = Router();
router.get('/tasks', requireAuth, async (req, res, next) => {
const parsed = listQuery.safeParse(req.query);
if (!parsed.success) {
return res.status(400).json({
type: 'about:blank',
title: 'Invalid query parameters',
status: 400,
errors: parsed.error.issues,
});
}
try {
const { data, nextCursor } = await taskService.list({
...parsed.data,
userId: req.user.id,
});
res.json({ data, nextCursor }); // shape matches the spec and the tests
} catch (err) {
next(err);
}
});

Dla endpointów opartych na bazie danych pojedynczo największy skok jakości daje przekazanie agentowi twojego prawdziwego schematu, zamiast zmuszania go do zgadywania. Serwer MCP dla Postgresa zamienia “wygeneruj endpoint zadań” z rusztowania na ślepo w kod dokładny wobec schematu — z właściwymi nazwami kolumn, typami i indeksami.

Bez niego: AI wymyśla taskService.list(), a ty tracisz rundę na poprawianie nazw pól wobec twoich rzeczywistych tabel.

Z nim: agent czyta żywy schemat, generuje pasujące do niego zapytania i sygnalizuje brakujący indeks za twoim filtrem status. Dla zespołów TypeScript Prisma Postgres MCP jest wbudowany w Prisma CLI i zarządza też migracjami:

Okno terminala
# Claude Code — register the Prisma Postgres MCP (schema + migrations)
claude mcp add prisma -- npx prisma mcp

Ten sam serwer rejestruje się w Cursor (Settings -> MCP) i Codex (~/.codex/config.toml) — konfiguracja MCP jest identyczna we wszystkich trzech narzędziach. Jeśli potrzebujesz tylko lekkiego, jednozadaniowego wzbogacenia — powiedzmy lintowania specyfikacji OpenAPI, a nie trwałego połączenia z bazą — Agent Skill jest lżejszym dopasowaniem: zainstaluj jeden z skills.sh poleceniem npx skills add <owner/repo> (uniwersalne CLI z vercel-labs/skills), które działa w Claude Code, Cursor i Codex.

Wygeneruj przekrojowe middleware raz i zaznacz wyraźnie, że kolejność ma znaczenie.

Sensem testów jest tu zamrożenie kształtu odpowiedzi i kontraktu błędów, by późniejsza edycja AI nie mogła ich po cichu zmienić.

Użyj trybu Agent, by wygenerować zestaw integracyjny, a potem uruchom go inline. W Settings -> Cursor Settings -> Agents -> Auto-Run dodaj npx vitest do listy dozwolonych poleceń, by zestaw działał bez pytania, i obserwuj diff: odrzucaj każdą zmianę, w której asertowane ciało testu rozjeżdża się z rzeczywistą odpowiedzią handlera. Edycja wieloplikowa w Cursor to idealne narzędzie do “zregeneruj handler i jego test razem, by kształty pozostały zsynchronizowane”.

Wygenerowany test integracyjny musi odzwierciedlać kształt { data, nextCursor } z handlera:

describe('GET /tasks', () => {
it('returns a cursor-paginated list', async () => {
const token = await getAuthToken();
const res = await request(app)
.get('/tasks?status=todo&limit=10')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.data).toBeInstanceOf(Array);
expect(['string', 'object']).toContain(typeof res.body.nextCursor); // string or null
});
});

Dla obciążenia wygeneruj skrypt k6 z jawnymi progami, by regresja przerywała przebieg, a nie tylko wyglądała na powolną:

import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/tasks`, {
headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
});
check(res, { 'status is 200': (r) => r.status === 200 });
}

Gdy wydzielasz v2, wygeneruj middleware wersjonowania i wyemituj sygnał deprecjacji z wyraźnie przyszłą datą wyłączenia.

app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
const deprecateV1 = (_req, res, next) => {
// RFC 9745: Deprecation to sf-date (RFC 9651) — uniksowy timestamp z prefiksem @, nie "true".
res.setHeader('Deprecation', '@1780617600'); // 2026-06-05, data deprecjacji v1
res.setHeader('Sunset', 'Wed, 31 Dec 2026 23:59:59 GMT'); // RFC 8594: HTTP-date
res.setHeader('Link', '<https://docs.example.com/migration-v2>; rel="deprecation"');
next();
};

Po każdej zmianie specyfikacji zregeneruj klientów, by konsumenci przesuwali się razem z tobą:

Okno terminala
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./sdk/typescript