Skip to main content
New: Forge AI docs + Loop PM assistant. 7-day free trial.
TemplateFREE⏱️ 90-120 minutes

OpenAPI Specification Template

A structured template for writing OpenAPI 3.1 specifications covering paths, schemas, authentication, error responses, and server configuration. Includes a filled example for a project management API.

By Tim Adair• Last updated 2026-03-05
OpenAPI Specification Template preview

OpenAPI Specification Template

Free OpenAPI Specification Template — open and start using immediately

or use email

Instant access. No spam.

What This Template Is For

An OpenAPI specification is a machine-readable description of your REST API. It defines every endpoint, request parameter, response schema, authentication method, and error format in a YAML or JSON file that tools can consume directly. Swagger UI generates interactive documentation from it. Code generators build client SDKs. Contract testing tools validate that your implementation matches the spec.

Writing the OpenAPI spec before implementation prevents two common problems. First, it creates a single source of truth that frontend engineers, QA, partner teams, and documentation tools all reference. Second, it forces you to design the complete API surface, including edge cases and error responses, before writing any handler code.

This template provides a structured approach to writing an OpenAPI 3.1 specification. It covers the info, servers, paths, components, and security sections with guidance on when and how to use each feature. If you need to design the API at a higher level before writing the formal spec, start with the API Design Specification Template. For how API documentation fits into the developer experience, see the Technical PM Handbook.

To understand how this spec connects to API-first design principles, review the glossary entry.


How to Use This Template

  1. Start by filling out the info section with your API's name, version, and description. This is the first thing consumers see in generated documentation.
  2. Define the servers section with your base URLs for production, staging, and sandbox environments.
  3. Design your schemas first (in the components/schemas section). Define every request body, response body, and shared type before writing path definitions.
  4. Write path definitions for each endpoint. Reference schemas from components/schemas rather than inlining them.
  5. Define security schemes in components/securitySchemes and apply them globally or per-endpoint.
  6. Document error responses with a shared error schema. Every endpoint should reference the same error format.
  7. Validate the spec with a linter (like Spectral or redocly lint) before sharing with consumers.

The Template

Info Section

openapi: "3.1.0"
info:
  title: "[API Name]"
  version: "[Semantic version, e.g., 1.0.0]"
  description: |
    [2-4 sentences describing what this API does, who it is for,
    and what the primary use cases are.]
  contact:
    name: "[Support team or person]"
    email: "[Support email]"
    url: "[Developer docs URL]"
  license:
    name: "[License name]"
    url: "[License URL]"
  x-api-id: "[Unique identifier for this API, e.g., UUID]"

Info section checklist:

  • Title is clear and matches the product name consumers know
  • Version follows semantic versioning (MAJOR.MINOR.PATCH)
  • Description explains what the API does in plain language
  • Contact info points to a real support channel
  • License is specified for external APIs

Servers

servers:
  - url: "https://api.example.com/v1"
    description: "Production"
  - url: "https://api.staging.example.com/v1"
    description: "Staging"
  - url: "https://api.sandbox.example.com/v1"
    description: "Sandbox (test data, no real charges)"

Server configuration notes:

  • Production URL uses HTTPS
  • Staging URL is accessible with test credentials
  • Sandbox environment resets data nightly (document this)
  • Version is included in the base URL path (if using URL versioning)

Security Schemes

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        [How to obtain a token. Link to auth documentation.]

    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        [How to obtain an API key. Link to dashboard.]

    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: "https://auth.example.com/authorize"
          tokenUrl: "https://auth.example.com/token"
          refreshUrl: "https://auth.example.com/token"
          scopes:
            "read:resources": "Read access to resources"
            "write:resources": "Write access to resources"
            "admin": "Full admin access"

# Apply globally (override per-endpoint if needed)
security:
  - bearerAuth: []

Shared Schemas

components:
  schemas:
    # Standard error response
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: "Machine-readable error code"
              example: "VALIDATION_ERROR"
            message:
              type: string
              description: "Human-readable error description"
              example: "The 'email' field is required"
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  issue:
                    type: string
            doc_url:
              type: string
              format: uri
              description: "Link to error documentation"

    # Pagination wrapper
    PaginatedResponse:
      type: object
      properties:
        data:
          type: array
          items: {}
        pagination:
          $ref: "#/components/schemas/Pagination"

    Pagination:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
        total_count:
          type: integer

    # Resource schemas
    "[ResourceName]":
      type: object
      required:
        - id
        - "[required_field]"
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          description: "Unique identifier"
        "[field_name]":
          type: string
          description: "[What this field represents]"
          example: "[Example value]"
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true

    "Create[ResourceName]Request":
      type: object
      required:
        - "[required_field]"
      properties:
        "[field_name]":
          type: string
          description: "[What this field represents]"
          minLength: 1
          maxLength: 255

    "Update[ResourceName]Request":
      type: object
      properties:
        "[field_name]":
          type: string
          description: "[What this field represents]"
          minLength: 1
          maxLength: 255

Schema design checklist:

  • All request schemas have validation constraints (minLength, maxLength, minimum, maximum, pattern)
  • All response schemas include example values for documentation
  • Read-only fields (id, created_at, updated_at) are marked with readOnly: true
  • Nullable fields use nullable: true (not type: ["string", "null"])
  • Enums list all valid values with descriptions
  • Shared schemas are defined in components/schemas and referenced via $ref

Path Definitions

paths:
  "/[resources]":
    get:
      summary: "List [resources]"
      operationId: "list[Resources]"
      tags:
        - "[Resource Group]"
      description: |
        [What this endpoint does. Mention pagination, filtering, and sorting.]
      parameters:
        - name: first
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
          description: "Number of results to return (max 100)"
        - name: after
          in: query
          schema:
            type: string
          description: "Cursor for pagination"
        - name: filter
          in: query
          schema:
            type: string
          description: "[Available filter fields and syntax]"
        - name: sort
          in: query
          schema:
            type: string
            enum: ["created_at", "-created_at", "name", "-name"]
          description: "Sort field (prefix with - for descending)"
      responses:
        "200":
          description: "Successful response"
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/[ResourceName]"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      summary: "Create a [resource]"
      operationId: "create[Resource]"
      tags:
        - "[Resource Group]"
      description: |
        [What this endpoint does. Mention required fields and side effects.]
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            format: uuid
          description: "Unique key for idempotent request handling"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Create[ResourceName]Request"
      responses:
        "201":
          description: "Resource created"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/[ResourceName]"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  "/[resources]/{id}":
    get:
      summary: "Get a [resource]"
      operationId: "get[Resource]"
      tags:
        - "[Resource Group]"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: "Successful response"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/[ResourceName]"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      summary: "Update a [resource]"
      operationId: "update[Resource]"
      tags:
        - "[Resource Group]"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Update[ResourceName]Request"
      responses:
        "200":
          description: "Resource updated"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/[ResourceName]"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      summary: "Delete a [resource]"
      operationId: "delete[Resource]"
      tags:
        - "[Resource Group]"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: "Resource deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

Shared Responses

components:
  responses:
    ValidationError:
      description: "Request validation failed"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "VALIDATION_ERROR"
              message: "Request validation failed"
              details:
                - field: "email"
                  issue: "Must be a valid email address"

    Unauthorized:
      description: "Authentication required"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "UNAUTHORIZED"
              message: "Invalid or missing authentication token"

    NotFound:
      description: "Resource not found"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "NOT_FOUND"
              message: "The requested resource does not exist"

    Conflict:
      description: "Resource conflict"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "CONFLICT"
              message: "A resource with this identifier already exists"

    RateLimited:
      description: "Too many requests"
      headers:
        Retry-After:
          schema:
            type: integer
          description: "Seconds until the rate limit resets"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "RATE_LIMITED"
              message: "Rate limit exceeded. Retry after 60 seconds."

Tags

tags:
  - name: "[Resource Group 1]"
    description: "[What this group of endpoints covers]"
  - name: "[Resource Group 2]"
    description: "[What this group of endpoints covers]"

OpenAPI Spec Validation Checklist

  • Spec passes redocly lint or Spectral with zero errors
  • Every path has at least one response defined
  • Every 4xx/5xx response uses the shared Error schema
  • Every list endpoint has pagination parameters
  • Every write endpoint documents the Idempotency-Key header
  • All schemas have examples for documentation rendering
  • operationId values are unique across the spec
  • Tags are used consistently to group related endpoints

Open Questions

#QuestionOwnerStatusDecision
1[Question][Name]Open
2[Question][Name]Open

Filled Example: TaskFlow API v1

openapi: "3.1.0"
info:
  title: "TaskFlow API"
  version: "1.0.0"
  description: |
    The TaskFlow API enables third-party integrations and internal clients
    to create, read, update, and manage projects and tasks. Primary consumers
    are the web/mobile frontend, Zapier integration, and 3 partner applications.
  contact:
    name: "TaskFlow Developer Support"
    email: "api-support@taskflow.io"
    url: "https://docs.taskflow.io"

servers:
  - url: "https://api.taskflow.io/v1"
    description: "Production"
  - url: "https://api.sandbox.taskflow.io/v1"
    description: "Sandbox (test data, resets daily)"

paths:
  /tasks:
    get:
      summary: "List tasks"
      operationId: "listTasks"
      tags: ["Tasks"]
      parameters:
        - name: project_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum: ["todo", "in_progress", "done"]
        - name: first
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: "List of tasks"

This excerpt shows the pattern. A complete TaskFlow spec would include 15-20 endpoints across Tasks, Projects, and Users resource groups.

Key Takeaways

  • Write the OpenAPI spec before implementation to use it as a design document and contract
  • Define schemas in components/schemas and reference them via $ref to avoid duplication
  • Document every error response with a consistent format and example values
  • Validate the spec with a linter in CI to catch structural issues before they reach consumers
  • Use contract testing to prevent drift between the spec and the implementation

About This Template

Created by: Tim Adair

Last Updated: 3/5/2026

Version: 1.0.0

License: Free for personal and commercial use

Frequently Asked Questions

When should I write the OpenAPI spec: before or after implementation?+
Write it before implementation. The spec serves as a design document that frontend engineers, partner teams, and QA can review and build against in parallel. If you write it after, you are documenting what the code happens to do rather than what it should do. The spec-first approach also enables code generation for server stubs and client SDKs, which accelerates both provider and consumer development.
Should I use YAML or JSON for the OpenAPI spec?+
YAML is more readable for humans and is the convention for hand-written specs. JSON is better for machine-generated specs and for embedding in code. Most teams write in YAML and let tools convert to JSON when needed. Whichever format you choose, commit the spec to version control alongside the source code and treat changes to it with the same rigor as code changes.
How do I keep the spec in sync with the implementation?+
Use contract testing. Tools like Prism (mock server from OpenAPI spec), Dredd (test runner that validates implementation against spec), and Schemathesis (property-based testing from OpenAPI) catch drift automatically. Run these tests in CI so that any divergence between the spec and the implementation blocks deployment. For more on contract testing, see the [API Testing Strategy Template](/templates/api-testing-template).
Should I use $ref for everything or inline some schemas?+
Use `$ref` for any schema referenced in more than one place. Inline schemas only for one-off request parameters that are unique to a single endpoint. Over-inlining creates duplicate definitions that drift out of sync. Over-referencing makes the spec hard to read because readers must jump between sections. The balance point: define all request/response body schemas and all shared types in `components/schemas`, and inline only simple query parameter schemas.
How do I handle API versioning in OpenAPI?+
Include the version in the `info.version` field and in the server URL path (`/v1/`). When you release a breaking change, create a new spec file for v2 (`openapi-v2.yaml`) rather than modifying the v1 spec. Keep the v1 spec frozen and maintained alongside the v1 implementation until that version is sunset. This gives consumers a stable reference for each version. ---

Explore More Templates

Browse our full library of AI-enhanced product management templates

Free PDF

Like This Template?

Subscribe to get new templates, frameworks, and PM strategies delivered to your inbox.

or use email

Instant PDF download. One email per week after that.

Want full SaaS idea playbooks with market research?

Explore Ideas Pro →