Setup and context — Why API-First Development Matters
In modern software development, parallel frontend and backend development is the norm. Yet when API contracts are shared only through verbal agreements or informal documentation, integration testing inevitably reveals a wave of mismatches and bugs.
API-first development means defining the API contract before writing any implementation code, then using that contract as the single source of truth throughout the development lifecycle. By centering your workflow around an OpenAPI specification, you can derive backend stubs, frontend type definitions, test cases, and published documentation — all from one specification file.
Antigravity's AI agents are exceptionally well-suited to this workflow. This article walks you through the complete API-first development pipeline: from designing OpenAPI specs, to automated code generation, contract testing, documentation deployment, and CI/CD integration.
If you're interested in API fundamentals and serverless architectures, check out Building Serverless APIs at Lightning Speed with Antigravity — Hono + Cloudflare Workers Practical Guide as well.
OpenAPI Specification Fundamentals and Design Principles
The Structure of OpenAPI 3.1
OpenAPI 3.1 achieved full JSON Schema compatibility and is the de facto standard as of 2026. Here's the basic structure:
# openapi.yaml — Your project's API specification
openapi: "3.1.0"
info:
title: "My SaaS API"
version: "1.0.0"
description: "Core API handling user management and billing"
servers:
- url: "https://api.example.com/v1"
description: "Production"
- url: "http://localhost:3000/v1"
description: "Local development"
paths:
/users:
get:
operationId: listUsers
summary: "List all users"
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
"200":
description: "Success"
content:
application/json:
schema:
$ref: "#/components/schemas/UserListResponse"
components:
schemas:
User:
type: object
required: [id, email, createdAt]
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
createdAt:
type: string
format: date-time
UserListResponse:
type: object
properties:
users:
type: array
items:
$ref: "#/components/schemas/User"
total:
type: integer
page:
type: integerDesign Principle: Think from the Consumer's Perspective
The key to great API design is adopting the consumer's viewpoint rather than the implementor's. When asking Antigravity's agent to draft a specification, including these principles in your prompt significantly improves quality:
- Resource-oriented: URL paths use nouns (
/users,/orders) while actions are expressed through HTTP methods - Consistent naming:
operationIdfollows averb + resourceconvention likelistUsers,getUser,createUser - Standardized errors: Define a common error structure using RFC 7807 (Problem Details) across all endpoints
- Pagination: Unify on either cursor-based or offset-based pagination and reuse definitions from
components/parameters
Crafting OpenAPI Specs Efficiently with Antigravity
AI Agent-Driven Spec Generation
The most efficient approach is to use Antigravity's Manager Surface to delegate spec creation to an agent:
# Antigravity prompt example
Create an OpenAPI 3.1 specification in YAML based on these requirements:
## Service Overview
- E-commerce product management API
- Authentication via Bearer token (JWT)
- Cursor-based pagination
## Endpoints
1. List products (with filtering and sorting)
2. Get product details
3. Create product (admin only)
4. Update product (admin only)
5. Delete product (admin only)
## Constraints
- Error responses follow RFC 7807
- All responses include rate-limit headers
- Use components/schemas for schema reuseAntigravity's agent references existing code in your project context, so if you already have model definitions (Prisma schemas, TypeORM entities, etc.), it automatically generates specifications consistent with them.
Inline Editing in Editor View
For fine-tuning spec details, Editor View's inline AI commands are invaluable. Open the spec file, place your cursor on a specific path or schema, and trigger Ctrl+I (inline command):
# Inline command example
"Add rate limiting response headers (X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset) to this endpoint"The AI understands the schema context and automatically adds header definitions to components/headers, creating $ref references from each response.
Building the Code Generation Pipeline
TypeScript Backend (Hono + Zod) Auto-Generation
Here's how to build a pipeline that generates TypeScript server code from your OpenAPI spec, combining openapi-typescript and openapi-zod-client:
# Install required packages
npm install openapi-typescript openapi-zod-client -D
# Generate type definitions
npx openapi-typescript openapi.yaml -o src/generated/api-types.ts
# Generate Zod schemas
npx openapi-zod-client openapi.yaml -o src/generated/api-schemas.tsHere's how to use the generated types in a Hono router:
// src/routes/users.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { listUsersQuerySchema, createUserBodySchema } from "../generated/api-schemas";
import type { paths } from "../generated/api-types";
// Type-safe response type
type ListUsersResponse =
paths["/users"]["get"]["responses"]["200"]["content"]["application/json"];
const users = new Hono();
users.get(
"/",
zValidator("query", listUsersQuerySchema),
async (c) => {
const { page, limit } = c.req.valid("query");
// Fetch users from DB (Prisma example)
const [items, total] = await Promise.all([
prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.user.count(),
]);
// Response is compatible with ListUsersResponse type
const response: ListUsersResponse = {
users: items,
total,
page,
};
return c.json(response);
}
);
export default users;Frontend API Client Auto-Generation
On the frontend side, openapi-fetch provides a type-safe API client generated from your spec:
// src/lib/api-client.ts
import createClient from "openapi-fetch";
import type { paths } from "../generated/api-types";
// Type-safe API client
const client = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL,
headers: {
Authorization: `Bearer ${getAccessToken()}`,
},
});
// Usage — paths, parameters, and responses are all type-safe
async function fetchUsers(page: number = 1) {
const { data, error } = await client.GET("/users", {
params: {
query: { page, limit: 20 },
},
});
if (error) {
// error type is also inferred from the OpenAPI spec
throw new ApiError(error);
}
// data.users — inferred as User[]
return data;
}The biggest advantage: updating your OpenAPI spec and regenerating types automatically synchronizes backend and frontend types.
Automated Test Generation with Antigravity Agents
Auto-Generating API Tests
Feed your OpenAPI spec to Antigravity's agent and have it generate comprehensive test suites:
# Antigravity prompt example
Generate Vitest test cases for every endpoint in openapi.yaml
covering these scenarios:
1. Happy path (200/201 responses)
2. Validation errors (400)
3. Authentication errors (401)
4. Authorization errors (403)
5. Not found (404)
6. Rate limiting (429)
Include clear descriptions and thorough assertions for each test.Here's an example of the generated test code:
// tests/api/users.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { createTestClient } from "../helpers/test-client";
describe("GET /users", () => {
const client = createTestClient();
it("returns paginated user list", async () => {
const res = await client.get("/users?page=1&limit=10");
expect(res.status).toBe(200);
expect(res.body.users).toBeInstanceOf(Array);
expect(res.body.users.length).toBeLessThanOrEqual(10);
expect(res.body.total).toBeTypeOf("number");
expect(res.body.page).toBe(1);
});
it("returns 400 when limit exceeds maximum", async () => {
const res = await client.get("/users?limit=999");
expect(res.status).toBe(400);
expect(res.body.type).toContain("validation-error");
// Verify RFC 7807 error response format
expect(res.body).toHaveProperty("title");
expect(res.body).toHaveProperty("detail");
});
it("returns 401 without authentication", async () => {
const unauthClient = createTestClient({ auth: false });
const res = await unauthClient.get("/users");
expect(res.status).toBe(401);
});
});
describe("POST /users", () => {
const adminClient = createTestClient({ role: "admin" });
it("allows admin to create a user", async () => {
const res = await adminClient.post("/users", {
body: {
email: "new@example.com",
name: "New User",
},
});
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
expect(res.body.email).toBe("new@example.com");
});
it("returns 403 for non-admin users", async () => {
const userClient = createTestClient({ role: "user" });
const res = await userClient.post("/users", {
body: { email: "test@example.com", name: "Test" },
});
expect(res.status).toBe(403);
});
});Contract Testing — Detecting Spec-Implementation Drift
The most critical aspect of API-first development is contract testing that guarantees your spec and implementation always match:
// tests/contract/openapi-contract.test.ts
import { describe, it, expect } from "vitest";
import SwaggerParser from "@apidevtools/swagger-parser";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import { createTestClient } from "../helpers/test-client";
describe("OpenAPI Contract Tests", () => {
let api: any;
let ajv: Ajv;
beforeAll(async () => {
// Parse the OpenAPI spec
api = await SwaggerParser.dereference("openapi.yaml");
ajv = new Ajv({ allErrors: true, strict: false });
addFormats(ajv);
});
it("GET /users response matches the specification", async () => {
const client = createTestClient();
const res = await client.get("/users");
// Extract schema from spec
const schema =
api.paths["/users"].get.responses["200"]
.content["application/json"].schema;
// Validate response against spec
const validate = ajv.compile(schema);
const valid = validate(res.body);
if (!valid) {
console.error("Schema violations:", validate.errors);
}
expect(valid).toBe(true);
});
});Integrating these contract tests into your CI pipeline automatically catches "updated the spec but forgot the implementation" and "changed the implementation but forgot the spec" scenarios.
Auto-Generating and Publishing API Documentation
Integrating Redoc / Swagger UI
Here's how to auto-generate beautiful API documentation from your spec and deploy it:
// src/app/api/docs/route.ts — Serve docs via Next.js API Route
import { readFileSync } from "fs";
import path from "path";
const specYaml = readFileSync(
path.join(process.cwd(), "openapi.yaml"),
"utf-8"
);
export async function GET() {
const html = `<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
<meta charset="utf-8" />
<link href="https://fonts.googleapis.com/css?family=Inter:300,400,600" rel="stylesheet">
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
</head>
<body>
<div id="redoc-container"></div>
<script>
Redoc.init('/api/docs/spec', {
theme: {
colors: { primary: { main: '#1a73e8' } },
typography: { fontFamily: 'Inter, sans-serif' },
},
hideDownloadButton: false,
expandResponses: '200,201',
}, document.getElementById('redoc-container'));
</script>
</body>
</html>`;
return new Response(html, {
headers: { "Content-Type": "text/html" },
});
}Improving Documentation Quality with Antigravity Agents
A prompt pattern for enhancing your spec's documentation quality:
# Antigravity prompt example
Improve the following aspects of openapi.yaml:
1. Add descriptions to all endpoints (including use cases)
2. Add example values to all parameters
3. Add realistic examples to all responses
4. Group endpoints using tags
5. Enrich security scheme descriptionsCI/CD Pipeline Integration
Automated Validation Pipeline with GitHub Actions
A pipeline that automatically triggers type generation, testing, and documentation builds whenever your OpenAPI spec changes:
# .github/workflows/api-first.yaml
name: API First Pipeline
on:
push:
paths:
- "openapi.yaml"
- "src/**"
pull_request:
paths:
- "openapi.yaml"
jobs:
validate-and-generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
# 1. Validate OpenAPI spec
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
# 2. Regenerate TypeScript types
- name: Generate TypeScript types
run: npx openapi-typescript openapi.yaml -o src/generated/api-types.ts
# 3. Regenerate Zod schemas
- name: Generate Zod schemas
run: npx openapi-zod-client openapi.yaml -o src/generated/api-schemas.ts
# 4. Verify generated files are committed
- name: Check for uncommitted generated files
run: |
git diff --exit-code src/generated/ || {
echo "❌ Generated files are out of date!"
echo "Run: npm run generate:api and commit the changes"
exit 1
}
# 5. Type checking
- name: TypeScript type check
run: npx tsc --noEmit
# 6. Contract tests
- name: Run contract tests
run: npx vitest run tests/contract/
# 7. API tests
- name: Run API tests
run: npx vitest run tests/api/Breaking Change Detection
Catch backward-incompatible API changes at the PR stage:
# Additional GitHub Actions step
- name: Detect breaking changes
if: github.event_name == 'pull_request'
run: |
# Fetch the base branch spec
git fetch origin main
git show origin/main:openapi.yaml > /tmp/openapi-base.yaml
# Detect incompatible changes
npx @redocly/cli diff /tmp/openapi-base.yaml openapi.yaml \
--fail-on-incompatibleThis configuration automatically blocks PRs that introduce breaking changes like removing required response fields or changing endpoint URLs.
Practical Design Patterns
Pattern 1: Versioning Strategy
# URL path-based versioning
servers:
- url: "https://api.example.com/v1"
- url: "https://api.example.com/v2"
# Header-based versioning
paths:
/users:
get:
parameters:
- name: API-Version
in: header
schema:
type: string
enum: ["2026-01-01", "2026-04-01"]
default: "2026-04-01"Pattern 2: Standardized Error Responses (RFC 7807)
components:
schemas:
ProblemDetail:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
description: "URI identifying the error type"
title:
type: string
description: "Human-readable error title"
status:
type: integer
description: "HTTP status code"
detail:
type: string
description: "Detailed error description"
instance:
type: string
format: uri
description: "URI for this specific error occurrence"
errors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: stringPattern 3: Cursor-Based Pagination
# Reusable definitions in components/parameters
components:
parameters:
CursorParam:
name: cursor
in: query
description: "Cursor value marking the start of the next page"
schema:
type: string
LimitParam:
name: limit
in: query
schema:
type: integer
default: 20
minimum: 1
maximum: 100
schemas:
PaginationMeta:
type: object
properties:
hasNext:
type: boolean
nextCursor:
type: string
nullable: true
total:
type: integerSummary
API-first development may seem like extra upfront work, but as development progresses, that investment yields exponential returns. By leveraging Antigravity's AI agents, you can rapidly iterate through spec creation, type generation, automated testing, and documentation publishing — fundamentally solving the eternal challenge of spec-implementation drift.
For deeper dives into security and authentication patterns, see Building Secure API Backends with Antigravity Agents — Authentication, Validation & Rate Limiting Patterns. If you're interested in comparing REST with GraphQL, check out Antigravity × GraphQL + Apollo Server Practical Tutorial as well.