How to split a large OpenAPI document into multiple files

by David Garcia, Principal consultant

So, you have written a large OpenAPI spec. At this point, you may have considered making the document modular by dividing it into smaller separate files.

In this article, we will break the Petstore example from the official OpenAPI documentation into smaller files.

A little side note before we start: This guide will be more helpful if you’ve documented an API project before by following the OpenAPI Specification. But don’t worry if you're starting a new OpenAPI document from scratch. You’ll find a skeleton project ready to be adapted at the end of the post 😊

Prerequisites

This guide assumes that you’re familiar with the OpenAPI Specification 3.0 (previously known as Swagger). If you haven't worked with the standard, I recommend you read first What is OpenAPI?. Then, try to write your first OpenAPI document.

Step 1 - Reusing responses

It’s easier to start splitting an OpenAPI document if you’re already reusing schemas.

Imagine that you need to document two endpoints: one to retrieve a group of pets, and a second one to retrieve a single pet. Consequently, you notice that you have to define the response object Pet twice.

paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          [...]
          content:
            application/json:    
              schema:
                  type: object
                  required:
                    - id
                    - name
                  properties:
                    id:
                      type: integer
                      format: int64
                    name:
                      type: string
                    tag:
                      type: string
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                  type: object
                  required:
                    - id
                    - name
                  properties:
                    id:
                      type: integer
                      format: int64
                    name:
                      type: string
                    tag:
                      type: string

Following this example, you define a separate schema named Pet under the section component/schemas, and reuse the object in both endpoints.

The keyword $ref does the trick. In general, a keyword allows us to reference other definitions within the same document.

Here's how the definition looks like using $ref:

paths:
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          schema:
            type: string
      responses:
        '200':
          description: Expected response to a valid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string

Step 2 - Reusing parameters

Now that you're comfortable reusing schemas in the same file let's reuse the parameter petId from the operation showPetById.

As in the previous step, move the parameter from the path to the components/parameters section and use $ref to reference the component. Here is an example:

paths:
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      parameters:
        - $ref: '#/components/parameters/petId'
[...]
components:
  parameters:
    petId:
        name: petId
        in: path
        required: true
        description: The id of the pet to retrieve
        schema:
            type: string

Step 3 - Importing definitions from a separate file

By reusing definitions, your file should now be shorter, even before starting to split it. If you're already pleased with the file organization, you can stop reading now... You’re still with me? Then let’s make our file more modular!

Do you remember the keyword we’ve been using to reuse content? $ref not only allows us to import objects from the same file but from other sources like a separate file or a remote URL as well.

Create a new file named schemas/Pet.yaml for the Pet schema. Then, move the definition to the new file. Here's how the resulting file should look like:

// schemas/Pet.yaml
type: object
required:
- id
- name
properties:
id:
  type: integer
  format: int64
name:
  type: string
tag:
  type: string

Now, point $ref to the new file's location.

// openapi.yaml
$ref: "./schemas/Pet.yaml"

Finally, do the same for the other objects that you might want to split into separate files.

Step 4 - Moving resources to a separate file

Until now, we’ve seen how to organize response objects and parameters. However, if the document defines several endpoints, you’ll likely still have a large file that's difficult to maintain. Next, we’ll organize the resource paths into multiple files.

For example, you could create the file path/pet.yaml. This file should define all the available operations, which their associated parameters and responses for the endpoint /pets/{petId}:

//paths/pets.yaml
get:
  summary: Info for a specific pet
  operationId: showPetById
  tags:
    - pets
  parameters:
    - $ref: "../parameters/path/petId.yaml"
  responses:
    '200':
      description: Expected response to a valid request
      content:
        application/json:
          schema:
            $ref: "../schemas/Pet.yaml"
    default:
      $ref: "../responses/UnexpectedError.yaml"

Then, import each path definition from the main OpenAPI document as we have been doing with the schemas and parameters.

//openapi.yaml
...
paths:
  /pets/{petId}:
      $ref: "./paths/pet.yaml

Finally, repeat the process for every other resource you want to import from a separate file.

Step 5 - Organizing the specification

Let's go one step further! We can split up the project even more to achieve better organization. Our goal is to end up with a main OpenAPI document as tiny as the following one:

// openapi.yaml
openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  description: Multi-file boilerplate for OpenAPI Specification.
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    $ref: "./paths/pets.yaml"
  /pets/{petId}:
    $ref: "./paths/pet.yaml"
components:
  parameters:
    $ref: "./parameters/_index.yaml"
  schemas:
    $ref: "./schemas/_index.yaml"
  responses:
    $ref: "./responses/_index.yaml"

To achieve this, create the following _index.yaml files:

  • parameters/_index.yaml
  • schemas/_index.yaml
  • responses/_index.yaml

Then, move to every file its definitions. For instance, ./schemas/_index.yaml for the Petstore example would look like:

Pet:
  $ref: "./Pet.yaml"
Pets:
  $ref: "./Pets.yaml"
Error:
  $ref: "./Error.yaml"

Step 6 - From many files to one

Some of the OpenAPI based tools support only a single file as an input. To continue using the spec with those tools, we’ll compile all the different files we’ve created with the command-line tool swagger-cli.

1. Open a new terminal. Then, install the package swagger-cli globally:

npm install -g swagger-cli

2. Run the command to merge all the files into one:

swagger-cli bundle openapi.yaml --outfile _build/openapi.yaml --type yaml

3. If everything goes well, you should see a single OpenAPI file compiled under the _build directory.

Putting all together

What a ride, eh? By splitting a large OpenAPI spec into multiple files, your project becomes much more maintainable. On top of that the documentation journey is more enjoyable as well. In my case, I’ve also noticed that other developers are more willing to contribute and propose changes to the document when it’s properly organized. You see, modular documentation makes everyone happy!

As promised, here’s my repository with the Petstore example divided into multiple files.

Repository: openapi-boilerplate

Feel free to reuse the project to define your OpenAPI document and review how the explanations from this article have been put into practice.

If you found this guide helpful, subscribe to our newsletter for more insightful content. Happy documenting!

Let's talk about docs.

Do you want to create great products for technical audiences? Dive into the topic with our articles on technical writing, developer experience, and Docs as Code.