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...
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.
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" }
You might also like
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.
Pagination
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
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.
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.
No spam. No contracts. Just a free demo.