Breaking a monolithic OpenAPI (Swagger) specification into smaller, manageable files is crucial for large APIs, but it can lead to a confusing mess if not done correctly. The core issue is maintaining a coherent, validated API definition across multiple linked files, ensuring that tools and developers can still understand the complete API contract.

Here’s how to effectively split your OpenAPI specs, focusing on practical implementation and common pitfalls.

The Problem with Monoliths

A single, massive openapi.yaml file for a complex API quickly becomes unwieldy. It’s hard to navigate, difficult for teams to collaborate on without constant merge conflicts, and challenging for tooling to process efficiently. Splitting it into smaller, logically grouped files is the solution, but it requires understanding OpenAPI’s referencing mechanisms.

The Solution: $ref and components

OpenAPI allows you to define reusable components (schemas, parameters, responses, etc.) in a components section. You can then reference these components from anywhere in your specification using the $ref keyword. This is the foundation for splitting your spec.

Instead of a single file, you’ll have a main openapi.yaml file that acts as the entry point, and then separate files for different categories of API elements.

Example Structure:

openapi.yaml
paths/
  users.yaml
  products.yaml
components/
  schemas/
    User.yaml
    Product.yaml
  parameters/
    UserIdParam.yaml
  responses/
    UserNotFoundResponse.yaml

Main openapi.yaml - The Orchestrator

Your main openapi.yaml file will define the overall API information and then use $ref to pull in definitions from other files.

openapi: 3.0.0
info:
  title: My Awesome API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
paths:
  /users:
    $ref: './paths/users.yaml'
  /products:
    $ref: './paths/products.yaml'
components:
  schemas:
    User:
      $ref: './components/schemas/User.yaml'
    Product:
      $ref: './components/schemas/Product.yaml'
  parameters:
    UserIdParam:
      $ref: './components/parameters/UserIdParam.yaml'
  responses:
    UserNotFoundResponse:
      $ref: './components/responses/UserNotFoundResponse.yaml'

Explanation:

  • paths/$ref: './paths/users.yaml' tells the parser to look in paths/users.yaml for the definitions related to the /users path.
  • components/schemas/User:$ref: './components/schemas/User.yaml' tells the parser to look in components/schemas/User.yaml for the definition of the User schema.

Defining Paths in Separate Files

Each file under the paths/ directory can define one or more API endpoints.

paths/users.yaml:

get:
  summary: Get a list of users
  operationId: listUsers
  responses:
    '200':
      description: A list of users.
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '../components/schemas/User.yaml' # Reference schema from components
post:
  summary: Create a new user
  operationId: createUser
  requestBody:
    required: true
    content:
      application/json:
        schema:
          $ref: '../components/schemas/User.yaml' # Reference schema from components
  responses:
    '201':
      description: User created successfully.
      content:
        application/json:
          schema:
            $ref: '../components/schemas/User.yaml'

Explanation:

  • Notice how paths/users.yaml references schemas defined in the components/ directory using relative paths (../components/schemas/User.yaml). This is key to keeping your structure organized.

Defining Reusable Components

The components/ directory holds all your reusable elements.

components/schemas/User.yaml:

type: object
properties:
  id:
    type: string
    format: uuid
    readOnly: true
  username:
    type: string
  email:
    type: string
    format: email
required:
  - username
  - email

components/parameters/UserIdParam.yaml:

name: userId
in: path
required: true
schema:
  type: string
  format: uuid
description: The ID of the user to operate on.

components/responses/UserNotFoundResponse.yaml:

description: User not found.
content:
  application/json:
    schema:
      type: object
      properties:
        message:
          type: string
          example: "User with ID 123 not found."

Linking Everything Together

Now, let’s see how a more complex path definition uses these components.

paths/products.yaml:

get:
  summary: Get a product by ID
  operationId: getProductById
  parameters:
    - $ref: '../components/parameters/ProductIdParam.yaml' # Assuming ProductIdParam exists
  responses:
    '200':
      description: Product details.
      content:
        application/json:
          schema:
            $ref: '../components/schemas/Product.yaml' # Assuming Product schema exists
    '404':
      $ref: '../components/responses/NotFoundResponse.yaml' # Assuming a generic NotFoundResponse

Tools and Validation

The most critical aspect of this approach is ensuring your tooling understands how to resolve these references.

  • Swagger Editor/UI/Codegen: Most modern OpenAPI tools are designed to handle these $ref resolutions automatically. When you open your main openapi.yaml in Swagger Editor, it should resolve all the relative paths and present a unified API definition.
  • Validation: To validate your entire spec, you need a tool that can resolve all the $refs before performing the validation. openapi-generator or spectral (with appropriate configuration) can often do this. A common command to validate a split spec using openapi-spec-validator (which needs PyYAML) might look like:
    python -m openapi_spec_validator openapi.yaml
    
    This command will attempt to resolve all $refs in openapi.yaml before validating against the OpenAPI 3.0.0 schema.

The Counterintuitive Power of Relative Paths

While it seems like a small detail, the use of relative paths for $ref is what makes this modularization truly robust and portable. Instead of absolute URLs or file paths that tie your spec to a specific directory structure, relative paths allow you to move the entire openapi.yaml and its associated files around as a self-contained unit. This means your entire API definition can be treated as a single, cohesive artifact, even though it’s composed of many smaller files, and can be easily versioned and shared.

Next Steps

Once you have your API definition cleanly split, the next logical step is to automate the generation of client SDKs and server stubs from this modular specification, ensuring consistency across your development ecosystem.

Want structured learning?

Take the full Swagger course →