In this article you'll learn how to design and implement a reusable swagger pagination solution to any RESTful API.
I assume you already know what swagger is, if not - read about it here.
And if you want to skip right to the solution have a look at the code or check out the live demo.
Our Example API - Dogs and Cats
Schemas
First, we'll define two schemas:
Dog
schema which represents a dog:
components:
schemas:
Dog:
type: object
properties:
id: { type: integer }
name: { type: string }
ownerName: { type: string }
favoriteToy: { type: string }
And Cat
schema which represents a cat:
components:
schemas:
Cat:
type: object
id: { type: integer }
name: { type: string }
servantName: { type: string }
favoriteFish: { type: string }
Endpoints
We also define two endpoints which returns lists all the dogs and cats in our database:
- GET
/dogs
- returns an array of all theDog
schemas - GET
/cats
- returns an array of all theCat
schemas
In swagger:
paths:
/dogs:
get:
tags:
- dog
summary: Find all dogs
responses:
200:
description: success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Dog'
/cats:
get:
tags:
- cats
summary: Find all cats
responses:
200:
description: success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Cat'
For example, if we call GET /dogs
we'll receive the following response from the api:
[
{ "id": 1, "name": "Spike", "ownerName": "Dave", "favoriteToy": "Ball"},
{ "id": 2, "name": "Lenny", "ownerName": "Michelle", "favoriteToy": "Rubber bone" },
{ "id": 3, "name": "Spike", "ownerName": "Ron","favoriteToy": "Ron's shoes"}
]
Oh no, too many dogs
But what happens if we return 100, 5000 or a million dogs in every GET /dogs
HTTP request? that's really bad for performance and takes a lot of time and resources.
To solve that, instead of returning an array of Dog
schemas, we'll use pagination:
- To every endpoint we'll add a
page
andper_page
query string parameters to indicate which page we need and how many results we want to be in every page returned. - We'll change our backend code of GET
/dogs
to return paginated result so that each call represents a single page from the list of all the items available in the db.
For example for GET /dogs?page=3&per_page=20
we'll receive:
{
"has_next": true, // do we have a another page after?
"has_prev": true, // do we have a page before?
"total": 100, // how many dogs we have in total in the db
"page": 3, // current page
"per_page": 20, // how many items per page the user requested
"pages": 5, // total number of pages
"results": [ // the results (20 in this case)
...
{ "id": 64, "name": "Rocky", "ownerName": "Lenny", "favoriteToy": "Ball"},
...
]
}
So now in swagger our get /dogs
endpoint will be:
paths:
/dogs:
get:
tags:
- dog
summary: Find all dogs
responses:
200:
description: success
content:
application/json:
schema:
type: object
properties:
total: { type: number }
page: { type: number }
per_page: { type: number }
has_next: { type: bool }
has_prev: { type: bool }
results:
type: array
items:
$ref: '#/components/schemas/Dog'
But what about the cats?
If you remember we have another endpoint - GET /cats
which now needs to be paginated as well, so we'll add pagination in the backend and change the cats endpoints to add paginated result as well:
paths:
/cats:
get:
tags:
- cat
summary: Find all cats
responses:
200:
description: success
content:
application/json:
schema:
type: object
properties:
total: { type: number } // duplicate definition
page: { type: number } // duplicate definition
per_page: { type: number } // duplicate definition
has_next: { type: bool } // duplicate definition
has_prev: { type: bool } // duplicate definition
results:
type: array
items:
$ref: '#/components/schemas/Cat'
This causes two issues:
- We need to repeat the same fields definition (
total
,per_page
,page
, ...) in all of our endpoints, this both duplicates code and prone to errors. - If in the future we'll want to change anything in the pagination schema (for example renaming
total
field tototalResults
or adding a new field) we'll have to do that for ALL the endpoints in our API manually.
So instead of repeating this structure in our endpoints we'll create a generic and reusable pagination schema that can be re-used across all the endpoints
How? keep reading
Generic pagination solution
We'll start by defining a generic PaginationResult
schema:
components:
schemas:
PaginatedResult:
type: object
properties:
total: { type: number }
page: { type: number }
per_page: { type: number }
has_next: { type: bool }
has_prev: { type: bool }
results: { type: array, items: {} } # any type of items
According to swagger's array documentation when we define an array items with {}
type, it means an array with arbitrary types (any kind)
Which is exactly what we need because every endpoint has it's own results
item's type in it.
Now all that's left is to combine the PaginatedResult
schema with the correct schema (Dog
, Cat
) in every endpoint.
This is done by using swagger's allOf keywords which combines several schemas together.
From swagger's documentation:
OpenAPI lets you combine and extend model definitions using the allOf keyword. allOf takes an array of object definitions that are used for independent validation but together compose a single object
We'll use it to combine two schemas for every endpoint:
- GET
/dogs
=PaginatedResult
+Dog
- GET
/cats
=PaginatedResult
+Cat
And now we'll receive a custom pagination solution for all the endpoints:
paths:
/dogs:
get:
tags:
- dog
summary: Find all dogs
parameters:
- { in: query, name: page, schema: { type: integer } }
- { in: query, name: per_page, schema: { type: integer } }
responses:
200:
description: success
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginatedResult"
- type: object
properties:
results:
type: array
items:
$ref: "#/components/schemas/Dog"
/cats:
get:
tags:
- cat
summary: Find all cats
parameters:
- { in: query, name: page, schema: { type: integer } }
- { in: query, name: per_page, schema: { type: integer } }
responses:
200:
description: success
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/PaginatedResult"
- type: object
properties:
results:
type: array
items:
$ref: "#/components/schemas/Cat"
Demo
To make sure this actually works I've created a small swagger website with the full example,
- Browse to it here: https://nitzano.github.io/pagination-swagger
- And The full code can be found in this github repo: https://github.com/nitzano/pagination-swagger
That's all, Hope you had fun and keep documenting!