← All articlesPlatform Engineering

Building Internal APIs with OpenAPI and Swagger: Design-First Approach

Design and build internal APIs using OpenAPI 3.1 specification. Code generation, validation middleware, interactive documentation, and contract-first...

Y
Yash Pritwani
14 min read

Design-First API Development

Most teams build APIs code-first: write the handler, then generate docs. The design-first approach reverses this: write the API specification first, then generate code, documentation, and client SDKs from it.

WebMobileIoTGatewayRate LimitAuthLoad BalanceTransformCacheService AService BService CDB / Cache

API gateway pattern: a single entry point handles auth, rate limiting, and routing to backend services.

Design-first benefits:

  • API design is reviewed before any code is written
  • Frontend and backend teams can work in parallel
  • Documentation is always in sync (it IS the source of truth)
  • Client SDKs are auto-generated
  • Request/response validation is automatic

OpenAPI 3.1 Specification

# openapi.yaml
openapi: 3.1.0
info:
  title: TechSaaS User API
  version: 1.0.0
  description: Internal user management API
  contact:
    name: TechSaaS API Team
    email: [email protected]

servers:
  - url: https://api.techsaas.cloud/v1
    description: Production
  - url: http://localhost:3000/v1
    description: Development

tags:
  - name: Users
    description: User management operations
  - name: Teams
    description: Team management

paths:
  /users:
    get:
      operationId: listUsers
      tags: [Users]
      summary: List all users
      description: Returns a paginated list of users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: search
          in: query
          description: Search by name or email
          schema:
            type
        - name: role
          in: query
          schema:
            type
            enum: [admin, member, viewer]
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      operationId: createUser
      tags: [Users]
      summary: Create a new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              name: Jane Doe
              email: [email protected]
              role: member
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          description: Email already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /users/{userId}:
    get:
      operationId: getUser
      tags: [Users]
      summary: Get user by ID
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      operationId: updateUser
      tags: [Users]
      summary: Update user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: User updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

    delete:
      operationId: deleteUser
      tags: [Users]
      summary: Delete user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type
            format: uuid
      responses:
        '204':
          description: User deleted
        '404':
          $ref: '#/components/responses/NotFound'

components:
  schemas:
    User:
      type: object
      required: [id, name, email, role, createdAt]
      properties:
        id:
          type
          format: uuid
        name:
          type
          minLength: 1
          maxLength: 255
        email:
          type
          format: email
        role:
          type
          enum: [admin, member, viewer]
        avatar:
          type
          format: uri
          nullable: true
        createdAt:
          type
          format: date-time
        updatedAt:
          type
          format: date-time

    CreateUserRequest:
      type: object
      required: [name, email]
      properties:
        name:
          type
          minLength: 1
          maxLength: 255
        email:
          type
          format: email
        role:
          type
          enum: [admin, member, viewer]
          default: member

    UpdateUserRequest:
      type: object
      minProperties: 1
      properties:
        name:
          type
          minLength: 1
          maxLength: 255
        email:
          type
          format: email
        role:
          type
          enum: [admin, member, viewer]

    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type
        message:
          type
        details:
          type: object

  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: UNAUTHORIZED
            message: Authentication required

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

security:
  - bearerAuth: []

Code Generation

Generate TypeScript Types

Get more insights on Platform Engineering

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

# Using openapi-typescript
npx openapi-typescript openapi.yaml -o src/api-types.ts

This generates fully typed interfaces:

// Generated: src/api-types.ts
export interface paths {
  "/users": {
    get: operations["listUsers"];
    post: operations["createUser"];
  };
  "/users/{userId}": {
    get: operations["getUser"];
    patch: operations["updateUser"];
    delete: operations["deleteUser"];
  };
}

export interface components {
  schemas: {
    User: {
      id
      name
      email
      role: "admin" | "member" | "viewer";
      avatar? | null;
      createdAt
      updatedAt?
    };
    // ... more types
  };
}

Generate Type-Safe Client SDK

# Using openapi-fetch (type-safe, minimal)
npm install openapi-fetch
// api-client.ts
import createClient from 'openapi-fetch';
import type { paths } from './api-types';

const client = createClient<paths>({
  baseUrl: 'https://api.techsaas.cloud/v1',
  headers: {
    Authorization: 'Bearer ' + getAccessToken(),
  },
});

// Fully type-safe API calls
const { data, error } = await client.GET('/users', {
  params: {
    query: { page: 1, limit: 20, role: 'admin' },
  },
});

if (data) {
  // data is typed as { data: User[], pagination: Pagination }
  data.data.forEach(user => console.log(user.name));
}

// Create user (request body is type-checked)
const { data: newUser } = await client.POST('/users', {
  body: {
    name: 'Jane Doe',
    email: '[email protected]',
    role: 'member',
  },
});

Request Validation Middleware

Automatically validate requests against the OpenAPI spec:

// Express middleware using express-openapi-validator
import * as OpenApiValidator from 'express-openapi-validator';

app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    validateResponses: true, // Also validate outgoing responses
  })
);

// Route handlers - no manual validation needed
app.get('/v1/users', async (req, res) => {
  // req.query is already validated:
  // - page is integer >= 1
  // - limit is integer 1-100
  // - role is one of admin/member/viewer
  const users = await userService.list(req.query);
  res.json(users);
});

app.post('/v1/users', async (req, res) => {
  // req.body is already validated:
  // - name is required, 1-255 chars
  // - email is required, valid format
  // - role is optional, valid enum value
  const user = await userService.create(req.body);
  res.status(201).json(user);
});

// Validation errors return 400 automatically:
// { "code": "VALIDATION_ERROR", "message": "email must be a valid email" }
API GatewayAuthServiceUserServiceOrderServicePaymentServiceMessage Bus / Events

Microservices architecture: independent services communicate through an API gateway and event bus.

Interactive Documentation

// Serve Swagger UI
import swaggerUi from 'swagger-ui-express';
import YAML from 'yaml';
import fs from 'fs';

const spec = YAML.parse(fs.readFileSync('./openapi.yaml', 'utf8'));

app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec, {
  customSiteTitle: 'TechSaaS API Docs',
  customCss: '.swagger-ui .topbar { display: none }',
}));

// Or use Scalar (modern, beautiful alternative)
// npm install @scalar/express-api-reference
import { apiReference } from '@scalar/express-api-reference';

app.use('/docs', apiReference({
  spec: { url: '/openapi.yaml' },
  theme: 'purple',
}));

API Design Best Practices

Consistent Error Format

{
  "code": "VALIDATION_ERROR",
  "message": "Invalid request body",
  "details": {
    "email": ["must be a valid email address"],
    "name": ["is required"]
  }
}

Versioning Strategy

URL path:    /v1/users (recommended for simplicity)
Header:      Accept: application/vnd.techsaas.v1+json
Query param: /users?version=1 (not recommended)

Free Resource

Free Cloud Architecture Checklist

A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.

Download the Checklist

Pagination

{
  "data": [...],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 156,
    "totalPages": 8
  }
}
TriggerwebhookIfSend EmailSMTPLog EventdatabaseUpdate CRMAPI callDonetruefalse

Workflow automation: triggers, conditions, and actions chain together to eliminate manual processes.

Response Envelope

Always wrap responses in a consistent envelope:

Success: { "data": ... }
List:    { "data": [...], "pagination": {...} }
Error:   { "code": "...", "message": "...", "details": {...} }

At TechSaaS, every internal API starts with an OpenAPI spec. The spec is the contract between frontend and backend teams — it gets reviewed in a PR before any code is written. We generate TypeScript types for the frontend, validation middleware for the backend, and interactive docs that serve as the living API documentation. This approach eliminates the "the API changed and nobody told the frontend team" problem.

#openapi#swagger#api-design#rest-api#code-generation

Related Service

Cloud Solutions

Let our experts help you build the right technology strategy for your business.

Need help with platform engineering?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.