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 inpaths/users.yamlfor the definitions related to the/userspath.components/schemas/User:$ref: './components/schemas/User.yaml'tells the parser to look incomponents/schemas/User.yamlfor the definition of theUserschema.
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.yamlreferences schemas defined in thecomponents/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
$refresolutions automatically. When you open your mainopenapi.yamlin 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-generatororspectral(with appropriate configuration) can often do this. A common command to validate a split spec usingopenapi-spec-validator(which needsPyYAML) might look like:
This command will attempt to resolve allpython -m openapi_spec_validator openapi.yaml$refs inopenapi.yamlbefore 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.