Back to Blog

API-First Design with OpenAPI 3.1: Schema Design, Code Generation, and Contract Testing

Design APIs first with OpenAPI 3.1 — schema design patterns, TypeScript code generation with openapi-typescript, contract testing with Prism, request validation

Viprasol Tech Team
May 23, 2026
13 min read

API-First Design with OpenAPI 3.1: Schema Design, Code Generation, and Contract Testing

API-first development inverts the typical workflow: instead of building an API and then documenting it, you write the OpenAPI specification first — then generate server types, client types, mock servers, and validation from that single source of truth.

The payoff: your API documentation is always accurate (generated, not written), your TypeScript types are consistent across client and server, and your tests can validate against the contract rather than implementation.


OpenAPI 3.1 Specification Structure

# openapi.yaml
openapi: 3.1.0

info:
  title: Acme API
  version: 1.0.0
  description: |
    REST API for Acme SaaS platform.
    
    ## Authentication
    All endpoints require Bearer token authentication.
    Obtain tokens via POST /auth/login.

servers:
  - url: https://api.yourapp.com/v1
    description: Production
  - url: https://api.staging.yourapp.com/v1
    description: Staging
  - url: http://localhost:3000/v1
    description: Local development

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    # Reusable schemas — define once, reference everywhere
    UUID:
      type: string
      format: uuid
      example: "550e8400-e29b-41d4-a716-446655440000"

    Timestamp:
      type: string
      format: date-time
      example: "2026-05-23T10:30:00Z"

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          example: "VALIDATION_ERROR"
        message:
          type: string
          example: "The request body is invalid"
        details:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string

    PaginatedMeta:
      type: object
      required: [total, page, perPage, totalPages]
      properties:
        total:
          type: integer
          example: 142
        page:
          type: integer
          example: 1
        perPage:
          type: integer
          example: 20
        totalPages:
          type: integer
          example: 8

    Project:
      type: object
      required: [id, name, status, createdAt, updatedAt]
      properties:
        id:
          $ref: '#/components/schemas/UUID'
        name:
          type: string
          minLength: 1
          maxLength: 255
          example: "Mobile App Redesign"
        description:
          type: string
          nullable: true
          maxLength: 5000
        status:
          type: string
          enum: [active, archived, deleted]
          example: "active"
        memberCount:
          type: integer
          minimum: 0
          example: 5
        createdAt:
          $ref: '#/components/schemas/Timestamp'
        updatedAt:
          $ref: '#/components/schemas/Timestamp'

    CreateProjectRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 5000

security:
  - BearerAuth: []

paths:
  /projects:
    get:
      operationId: listProjects
      summary: List projects
      tags: [Projects]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: perPage
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [active, archived]
      responses:
        '200':
          description: Paginated list of projects
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Project'
                  meta:
                    $ref: '#/components/schemas/PaginatedMeta'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    post:
      operationId: createProject
      summary: Create a project
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '201':
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

TypeScript Code Generation

Generate types from your OpenAPI spec — never write API types manually:

# Install openapi-typescript
npm install -D openapi-typescript

# Generate types from spec
npx openapi-typescript openapi.yaml -o src/types/api.d.ts
// src/types/api.d.ts (generated — do not edit manually)
export interface paths {
  '/projects': {
    get: operations['listProjects'];
    post: operations['createProject'];
  };
  '/projects/{projectId}': {
    get: operations['getProject'];
    patch: operations['updateProject'];
    delete: operations['deleteProject'];
  };
}

export interface components {
  schemas: {
    Project: {
      id: string;
      name: string;
      description?: string | null;
      status: 'active' | 'archived' | 'deleted';
      memberCount: number;
      createdAt: string;
      updatedAt: string;
    };
    CreateProjectRequest: {
      name: string;
      description?: string;
    };
    Error: {
      code: string;
      message: string;
      details?: Array<{ field?: string; message?: string }>;
    };
  };
}

export type operations = {
  listProjects: {
    parameters: {
      query?: {
        page?: number;
        perPage?: number;
        status?: 'active' | 'archived';
      };
    };
    responses: {
      200: {
        content: {
          'application/json': {
            data: components['schemas']['Project'][];
            meta: { total: number; page: number; perPage: number; totalPages: number };
          };
        };
      };
    };
  };
};
// Use generated types in your API client
import type { components, operations } from './types/api';

type Project = components['schemas']['Project'];
type CreateProjectRequest = components['schemas']['CreateProjectRequest'];
type ListProjectsParams = operations['listProjects']['parameters']['query'];

// Type-safe API client
export async function listProjects(
  params: ListProjectsParams = {}
): Promise<{ data: Project[]; meta: { total: number; page: number; perPage: number; totalPages: number } }> {
  const searchParams = new URLSearchParams(
    Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])
  );
  const response = await fetch(`/api/v1/projects?${searchParams}`, {
    headers: { Authorization: `Bearer ${getToken()}` },
  });
  if (!response.ok) throw new ApiError(await response.json());
  return response.json();
}

🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Request Validation Middleware

Validate all incoming requests against your OpenAPI spec automatically:

// middleware/openapi-validation.ts
import { OpenApiValidator } from 'express-openapi-validator';
// or for Fastify:
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import * as yaml from 'js-yaml';
import { readFileSync } from 'fs';

const spec = yaml.load(readFileSync('./openapi.yaml', 'utf8')) as Record<string, unknown>;

// Fastify: compile validators from OpenAPI schemas
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
addFormats(ajv);

// Compile request body validator for createProject
const validateCreateProject = ajv.compile({
  type: 'object',
  required: ['name'],
  properties: {
    name: { type: 'string', minLength: 1, maxLength: 255 },
    description: { type: 'string', maxLength: 5000 },
  },
  additionalProperties: false,
});

app.post('/v1/projects', async (request, reply) => {
  // Validate request body against OpenAPI schema
  if (!validateCreateProject(request.body)) {
    return reply.code(400).send({
      code: 'VALIDATION_ERROR',
      message: 'The request body is invalid',
      details: validateCreateProject.errors?.map(e => ({
        field: e.instancePath.replace('/', ''),
        message: e.message,
      })),
    });
  }

  // ... route handler
});

// Better: use fastify-openapi-glue to auto-generate routes from spec
import fastifyOpenapiGlue from 'fastify-openapi-glue';

app.register(fastifyOpenapiGlue, {
  specification: './openapi.yaml',
  serviceHandlers: './src/handlers',
  // Auto-validates all requests and responses against spec
});

Contract Testing with Prism

Prism runs a mock server from your OpenAPI spec — frontend teams can develop before the backend exists:

# Install Prism
npm install -g @stoplight/prism-cli

# Start mock server
prism mock openapi.yaml --port 4010

# All endpoints from your spec are immediately available
curl http://localhost:4010/v1/projects
# Returns: realistic mock data matching your schema

# Validate that your real server conforms to the spec
prism proxy openapi.yaml http://localhost:3000 --port 4020
# Sends requests to real server, validates responses match spec
# Reports any schema violations
// tests/contract.test.ts — verify your server matches the spec
import { createServer } from '@stoplight/prism-http';

describe('API Contract Tests', () => {
  let prism: Awaited<ReturnType<typeof createServer>>;

  beforeAll(async () => {
    prism = await createServer(spec, {
      mock: { dynamic: true },
      validateRequest: true,
      validateResponse: true,
    });
  });

  it('GET /projects returns valid schema', async () => {
    const response = await fetch('http://localhost:3000/v1/projects', {
      headers: { Authorization: 'Bearer test-token' },
    });

    // Validate response against OpenAPI schema
    const result = await prism.request({
      method: 'GET',
      url: { path: '/projects', query: {} },
      headers: { authorization: 'Bearer test-token' },
    }, response);

    expect(result.violations).toHaveLength(0);
  });
});

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Keeping Spec and Code in Sync

# .github/workflows/api-spec.yml
name: API Spec Validation

on: [pull_request]

jobs:
  validate-spec:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Validate OpenAPI spec is valid
      - name: Validate OpenAPI spec
        run: npx @redocly/cli lint openapi.yaml

      # Check generated types are up to date
      - name: Regenerate types
        run: npx openapi-typescript openapi.yaml -o src/types/api.gen.d.ts

      - name: Check for type drift
        run: git diff --exit-code src/types/api.d.ts
        # Fails if developer changed the generated file manually

      # Run contract tests against the spec
      - name: Contract tests
        run: |
          npx prism mock openapi.yaml --port 4010 &
          sleep 2
          npm run test:contract

API Versioning Strategy

# Versioning in OpenAPI
servers:
  - url: https://api.yourapp.com/v1
    description: Current stable API

  - url: https://api.yourapp.com/v2
    description: Next version (beta)

# Header-based versioning alternative:
# All requests to /api/* with header: API-Version: 2026-05
# Stripe uses this pattern

Working With Viprasol

We design APIs with OpenAPI-first methodology — specification design, TypeScript code generation, validation middleware, mock servers for parallel development, and documentation hosted with Redoc or Swagger UI. Well-designed APIs accelerate frontend development and reduce integration errors.

Talk to our team about API design and documentation.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.