Skip to main content
AllDevToolsHub
2026-06-19
Last reviewed: Jun 2026
API
Est Read: 07_MIN

OpenAPI 3.1 in Practice: Design-First APIs That Teams Actually Use

OpenAPI 3.1 in Practice: Design-First APIs That Teams Actually Use
Processing_Node: 01

#1OpenAPI 3.1 design-first APIs that stay current

What we tested: We evaluated the tools and techniques described in this article using real developer workflows. All browser-based tools on this site run locally, your data never leaves your machine.

OpenAPI 3.1 only helps when the spec is the thing the team actually maintains.

The practical goal is simple: keep the contract current, use it to drive tests and code, and stop the drift that makes most specs useless.


#2Why Design-First Matters

There are two ways to work with OpenAPI:

Code-first: write the API, generate the spec from annotations. Fast to start, but the spec becomes stale because it is a build artifact, not the canonical source. Developers stop trusting it.

Design-first: write the OpenAPI spec first, use it to generate server stubs and clients, maintain the spec as the source of truth. Slower to start, but the spec stays accurate because all changes go through it.

Design-first wins at scale. When you have multiple teams, multiple client apps, or external API consumers, an accurate, living spec is the difference between smooth integrations and weeks of debugging.


#2What Changed in OpenAPI 3.1

OpenAPI 3.1 (released 2021, now the dominant version in 2026) has two headline changes:

#3Full JSON Schema alignment

OpenAPI 3.0 used a modified, incompatible subset of JSON Schema. 3.1 uses JSON Schema 2020-12 directly. This means:

yaml
# ❌ OpenAPI 3.0, nullable field (custom extension)
email:
  type: string
  nullable: true

# ✅ OpenAPI 3.1, standard JSON Schema
email:
  type: ['string', 'null']
  # or
email:
  oneOf:
    - type: string
    - type: 'null'

You can now use $ref alongside other keywords:

yaml
# ❌ OpenAPI 3.0, $ref alone, cannot add description or required
$ref: '#/components/schemas/User'

# ✅ OpenAPI 3.1, $ref with sibling keywords
allOf:
  - $ref: '#/components/schemas/User'
description: "The authenticated user"

#3Webhooks support

yaml
openapi: '3.1.0'
webhooks:
  paymentCompleted:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentEvent'
      responses:
        '200':
          description: Webhook received

#2Writing a Maintainable OpenAPI Spec

A spec that gathers dust helps no one. These patterns keep it alive:

#3Start minimal, expand iteratively

yaml
openapi: '3.1.0'
info:
  title: My API
  version: '1.0.0'
  description: |
    ## Authentication
    All endpoints require `Authorization: Bearer <token>` header.
    
    ## Rate Limiting
    100 requests per minute per API key. Returns 429 when exceeded.
    
    ## Errors
    All errors follow the RFC 7807 Problem Details format.

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

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers        # use camelCase operationIds, they become function names in clients
      tags: [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
      responses:
        '200':
          description: Paginated user list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

#3Keep components DRY

yaml
components:
  schemas:
    # Base user, used in list responses
    UserSummary:
      type: object
      required: [id, email, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        createdAt:
          type: string
          format: date-time

    # Full user, used in single-resource responses
    User:
      allOf:
        - $ref: '#/components/schemas/UserSummary'
        - type: object
          properties:
            profile:
              $ref: '#/components/schemas/UserProfile'
            role:
              type: string
              enum: [admin, user, viewer]

    # Paginated envelope, reused for all list responses
    PaginatedResponse:
      type: object
      required: [data, pagination]
      properties:
        pagination:
          type: object
          required: [page, limit, total]
          properties:
            page: { type: integer }
            limit: { type: integer }
            total: { type: integer }

    UserListResponse:
      allOf:
        - $ref: '#/components/schemas/PaginatedResponse'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/UserSummary'

  responses:
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until rate limit resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProblemDetail'

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

security:
  - bearerAuth: []

#2Generating TypeScript Clients

The main payoff of a maintained spec: never write an API client by hand again.

#3With openapi-typescript (types only, no runtime)

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

This generates TypeScript interfaces. Combine with openapi-fetch for a typed fetch client:

typescript
import createClient from 'openapi-fetch';
import type { paths } from './api/types';

const client = createClient<paths>({ baseUrl: 'https://api.example.com/v1' });

// Fully typed, params, body, and response
const { data, error } = await client.GET('/users', {
  params: { query: { page: 1, limit: 20 } },
});

if (data) {
  console.log(data.data[0].email); // TypeScript knows the shape
}

#3With openapi-generator-cli (full SDK)

bash
npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o src/api/generated \
  --additional-properties=supportsES6=true

#2Contract Testing

Contract tests verify that your actual running API matches the spec, catching drift before it reaches production.

#3Dredd

bash
npm install --save-dev dredd

# Run against local server
dredd openapi.yaml http://localhost:3000/v1

#3Custom Vitest contract tests

typescript
// tests/contract/spec-compliance.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import SwaggerParser from '@apidevtools/swagger-parser';
import Ajv from 'ajv';

let spec: any;
let ajv: Ajv;

beforeAll(async () => {
  spec = await SwaggerParser.dereference('openapi.yaml');
  ajv = new Ajv({ allErrors: true });
});

describe('GET /users contract', () => {
  it('response matches schema', async () => {
    const res = await fetch('http://localhost:3000/v1/users');
    const data = await res.json();
    
    const schema = spec.paths['/users'].get.responses['200']
      .content['application/json'].schema;
    
    const validate = ajv.compile(schema);
    expect(validate(data)).toBe(true);
    if (!validate(data)) {
      console.error(ajv.errorsText(validate.errors));
    }
  });
});

#2Best Free Tools for OpenAPI in 2026

ToolBest forFree tier
Swagger EditorEditing with inline validationFull
Stoplight StudioVisual design-first authoringFree tier
RedocBeautiful API documentationFull OSS
ScalarModern interactive docsFull OSS
AllDevToolsHub API TesterTesting endpoints from browserFull

#2Common Mistakes

  • Using type: string\nnullable: true (3.0 syntax) in a 3.1 spec. Validators will accept it but clients and generators will handle it incorrectly. Use type: ['string', 'null'] in 3.1.
  • Not using operationId. Without operationId, code generators create ugly function names from the path and method. Use camelCase operationIds like listUsers, createUser, getUserById.
  • One massive YAML file. Split large specs using $ref to external files. Most validators and generators support multi-file specs.
  • Only documenting the happy path. Document every 4xx and 5xx response your API can return. Contract tests cannot catch undocumented errors.
  • Checking the spec into the same PR as the implementation. This defeats the design-first principle. The spec PR should be reviewed and approved first; the implementation PR links to it.

#2Frequently Asked Questions

#3Should I use OpenAPI 3.0 or 3.1?

Use 3.1 for new projects. The JSON Schema alignment alone makes it worth it, you get better tooling, correct nullable handling, and accurate codegen. Only stick with 3.0 if a critical tool in your workflow (like an older AWS API Gateway version) does not support 3.1 yet.

#3How do I handle authentication in OpenAPI?

Define securitySchemes in components and apply globally with security: at the root level. Override at the operation level for endpoints that don't require auth (like POST /auth/login). See the example above.

#3Can I generate the spec from my existing Express/FastAPI code?

Yes, code-first tools like tsoa (TypeScript), FastAPI (Python), or swagger-jsdoc generate specs from annotations. This is a valid approach when you have a large existing codebase. The trade-off is that the spec is a generated artifact, so it requires discipline to keep annotations up-to-date.

#3How do I share the spec with frontend teams?

Commit openapi.yaml to the repo root. Add a CI step that validates it on every PR using npx @redocly/cli lint openapi.yaml. Set up auto-generated docs on a /docs route using Redoc or Scalar. Publish the TypeScript client as an internal npm package generated from the spec.

Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.

#2Sources / Further reading

#2Try These Tools on AllDevToolsHub

  • OpenAPI Validator — Validate OpenAPI 3.1 specs against the official schema to catch structural errors
  • API Client Generator — Generate type-safe API clients from OpenAPI specs for TypeScript, Python, and more
  • REST API Tester — Test API endpoints designed from your OpenAPI spec with configurable requests

All tools run entirely in your browser. No sign-up, no data upload, no server round-trips.

#2Try These Tools

Quick Summary

>- A practical guide to OpenAPI 3.1 in 2026. Covers the JSON Schema alignment improvements, writing contracts your team will maintain, generating server stubs and TypeScript clients, automated contract testing, and the best free tools for editing and validating OpenAPI specs without leaving your browser.

Key Takeaways

Key Takeaways

  • Design-first API development starts with the OpenAPI specification — write the spec before writing any code, then generate servers, clients, and docs from it.
  • OpenAPI 3.1 aligns with JSON Schema draft 2020-12 — this means full support for exclusiveMinimum, type arrays, and $ref across schemas.
  • Code generation from OpenAPI specs ensures your implementation matches the contract — tools like openapi-generator produce type-safe clients and server stubs.
Use Cases

When to use it

  • Designing a REST API contract before implementation — share the OpenAPI spec with frontend and backend teams for parallel development.
  • Generating type-safe TypeScript clients from an OpenAPI spec for frontend consumption.
  • Validating API requests and responses against the OpenAPI spec in CI/CD to catch contract violations.
Watch out

Common Mistakes

  • Writing the OpenAPI spec after the code — this leads to specs that drift from the actual implementation.
  • Not validating the spec before code generation — errors in the spec propagate to all generated code.
  • Using OpenAPI 3.0 features when 3.1 is available — 3.1 adds JSON Schema compatibility, webhook definitions, and improved $ref handling.
FAQ

OpenAPI 3.1 in Practice: Design-First APIs That Teams Actually Use, Frequently Asked

What is design-first API development?

Design-first means writing the OpenAPI specification before implementing the API. The spec becomes the contract that drives code generation, documentation, testing, and client SDK creation. This ensures the API is designed for consumers first.

What is new in OpenAPI 3.1 vs 3.0?

OpenAPI 3.1 aligns with JSON Schema 2020-12 (full compatibility), adds webhook definitions, supports $ref alongside other keywords, and introduces components.pathItems for reusable path definitions.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-06-19Last reviewed 2026-06-19

Tools Mentioned in This Article

Tools, tactics, and toughened-up tips, once a week

New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.

Found an error or have feedback?

We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.

Last reviewed: 2026-06-19
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.