OpenAPI Specification
Now we need an OpenAPI specification that describes the API contract before we write any implementation code. Given a Film with the following structure:
{
"title": "ACADEMY DINOSAUR",
"description": "An Epic Drama of a Feminist And a Mad Scientist",
"releaseYear": 2006,
"rating": "PG",
"length": 86,
"language": "English",
"originalLanguage": "English",
"rentalDuration": 3,
"rentalRate": 4.99,
"replacementCost": 20.99,
"specialFeatures": "Trailers,Deleted Scenes",
"id": 42,
"lastUpdate": "2006-02-15T04:03:42Z"
}
We want our Spring Boot application to expose CRUD endpoints
Changed files
.
├── ...
└── src
├── main
│ ├── ...
│ └── resources
│ ├── ...
│ ├── openapi.yaml
│ └── ...
└── test
└── ...
OpenAPI Specification
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to HTTP APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.
- Java
- Kotlin
- Groovy
openapi: 3.0.3
info:
title: Sakila API
version: 1.0.0
description: API for the Sakila sample database - A DVD rental store management system
contact:
name: Pollito
url: https://springboot.pollito.tech/about/about-the-author
servers:
- url: http://localhost:8080/api
description: dev
- url: https://sakila-java.pollito.tech/api
description: prod
tags:
- name: Films
description: Endpoints related to films
paths:
/films:
get:
tags:
- Films
operationId: getFilms
summary: Lists films
description: Returns a standard response where the data property is a page of films matching the parameters criteria
parameters:
- name: page
in: query
description: Page number (0-based index)
schema:
type: integer
default: 0
minimum: 0
required: false
- name: size
in: query
description: Number of items per page
schema:
type: integer
default: 10
minimum: 1
maximum: 100
required: false
- name: sort
in: query
description: Sort criteria format `property,asc|desc`. Multiple sort parameters allowed
schema:
type: array
items:
type: string
example: "name,asc"
style: form
explode: true
required: false
responses:
'200':
description: Fetched page of films
content:
application/json:
schema:
$ref: '#/components/schemas/FilmListResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- Films
operationId: createFilm
summary: Creates a new film
description: Creates a new film. Returns a standard response where the data property is the created film
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'201':
description: Created Film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/films/{id}:
get:
tags:
- Films
operationId: getFilm
summary: Finds a film by id
description: Returns a standard response where the data property is the film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'200':
description: Fetched film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
tags:
- Films
operationId: updateFilm
summary: Updates a film
description: Updates the fields of a film. Returns a standard response where the data property is the updated film
parameters:
- $ref: '#/components/parameters/filmId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'200':
description: Updated film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags:
- Films
operationId: deleteFilm
summary: Deletes a film
description: Deletes a film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'204':
description: Deleted film. No content is returned
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
parameters:
filmId:
name: id
in: path
description: Film unique identifier
required: true
schema:
type: integer
minimum: 1
schemas:
Error:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
description: Standard error response
properties:
title:
type: string
description: HTTP Reason Phrase
example: 'Not Found'
detail:
type: string
description: Description of the problem
example: "No static resource for request '/'."
required:
- title
Film:
description: A film
allOf:
- $ref: '#/components/schemas/FilmFields'
- type: object
properties:
id:
type: integer
description: Unique identifier of the film
example: 42
minimum: 1
lastUpdate:
type: string
format: date-time
description: When the film record was last updated
example: '2006-02-15T04:03:42Z'
required:
- id
- lastUpdate
FilmFields:
type: object
description: Writable fields for a film (all required fields specified)
properties:
title:
type: string
description: Title of the film
example: 'ACADEMY DINOSAUR'
maxLength: 255
description:
type: string
description: Short synopsis of the film
example: 'An Epic Drama of a Feminist And a Mad Scientist'
maxLength: 255
releaseYear:
type: integer
description: Year the film was released
example: 2006
minimum: 1800
maximum: 2100
rating:
$ref: '#/components/schemas/FilmRating'
length:
type: integer
description: Duration of the film in minutes
example: 86
minimum: 1
language:
$ref: '#/components/schemas/FilmLanguage'
originalLanguage:
$ref: '#/components/schemas/FilmLanguage'
rentalDuration:
type: integer
description: Length of the rental period in days
example: 3
minimum: 0
rentalRate:
type: number
format: double
multipleOf: 0.01
description: Cost to rent the film for the rental period
example: 4.99
minimum: 0
replacementCost:
type: number
format: double
multipleOf: 0.01
description: Amount charged if the film is not returned or is returned damaged
example: 20.99
minimum: 0
specialFeatures:
type: string
description: Special features available on the film DVD
example: 'Trailers,Deleted Scenes'
maxLength: 255
required:
- title
- language
- rentalDuration
- rentalRate
- replacementCost
FilmLanguage:
type: string
enum: [English, Italian, Japanese, Mandarin, French, German]
description: Language used in the film
example: 'English'
FilmRating:
type: string
enum: [G, PG, PG-13, R, NC-17]
description: Motion Picture Association (MPA) rating
example: 'PG'
FilmListResponse:
description: Standard response where the data property is a Page of Films
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
allOf:
- $ref: "#/components/schemas/Page"
- type: object
properties:
content:
items:
$ref: "#/components/schemas/Film"
type: array
required:
- data
FilmResponse:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
$ref: '#/components/schemas/Film'
required:
- data
Page:
type: object
description: Sublist of a list of objects. It allows to gain information about the position of it in the containing entire list
properties:
content:
default: []
items: {}
type: array
pageable:
$ref: "#/components/schemas/Pageable"
totalElements:
default: 0
description: Total number of items that meet the criteria
example: 10
type: integer
totalPages:
default: 0
description: Total pages of items that meet the criteria
example: 10
type: integer
required:
- content
- pageable
- totalElements
- totalPages
Pageable:
type: object
description: Pagination information
properties:
pageNumber:
description: Current page number (starts from 0)
example: 0
minimum: 0
type: integer
pageSize:
description: Number of items retrieved on this page
example: 10
minimum: 0
type: integer
required:
- pageNumber
- pageSize
ResponseMetadata:
type: object
description: Standard response
properties:
instance:
type: string
description: API endpoint that was called
example: '/api/something'
status:
type: integer
description: HTTP status code
example: 200
minimum: 100
maximum: 599
timestamp:
type: string
format: date-time
description: ISO 8601 timestamp of when the response was generated
example: '2026-01-03T17:11:50.826722328Z'
trace:
type: string
description: Unique trace identifier for debugging purposes. Under unexpected situations it can be '00000000000000000000000000000000'
example: '9482c151-b417-43ff-9dbb-ee12b84e5d99'
required:
- instance
- timestamp
- trace
- status
openapi: 3.0.3
info:
title: Sakila API
version: 1.0.0
description: API for the Sakila sample database - A DVD rental store management system
contact:
name: Pollito
url: https://springboot.pollito.tech/about/about-the-author
servers:
- url: http://localhost:8080/api
description: dev
- url: https://sakila-kotlin.pollito.tech/api
description: prod
tags:
- name: Films
description: Endpoints related to films
paths:
/films:
get:
tags:
- Films
operationId: getFilms
summary: Lists films
description: Returns a standard response where the data property is a page of films matching the parameters criteria
parameters:
- name: page
in: query
description: Page number (0-based index)
schema:
type: integer
default: 0
minimum: 0
required: false
- name: size
in: query
description: Number of items per page
schema:
type: integer
default: 10
minimum: 1
maximum: 100
required: false
- name: sort
in: query
description: Sort criteria format `property,asc|desc`. Multiple sort parameters allowed
schema:
type: array
items:
type: string
example: "name,asc"
style: form
explode: true
required: false
responses:
'200':
description: Fetched page of films
content:
application/json:
schema:
$ref: '#/components/schemas/FilmListResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- Films
operationId: createFilm
summary: Creates a new film
description: Creates a new film. Returns a standard response where the data property is the created film
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'201':
description: Created Film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/films/{id}:
get:
tags:
- Films
operationId: getFilm
summary: Finds a film by id
description: Returns a standard response where the data property is the film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'200':
description: Fetched film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
tags:
- Films
operationId: updateFilm
summary: Updates a film
description: Updates the fields of a film. Returns a standard response where the data property is the updated film
parameters:
- $ref: '#/components/parameters/filmId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'200':
description: Updated film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags:
- Films
operationId: deleteFilm
summary: Deletes a film
description: Deletes a film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'204':
description: Deleted film. No content is returned
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
parameters:
filmId:
name: id
in: path
description: Film unique identifier
required: true
schema:
type: integer
minimum: 1
schemas:
Error:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
description: Standard error response
properties:
title:
type: string
description: HTTP Reason Phrase
example: 'Not Found'
detail:
type: string
description: Description of the problem
example: "No static resource for request '/'."
required:
- title
Film:
description: A film
allOf:
- $ref: '#/components/schemas/FilmFields'
- type: object
properties:
id:
type: integer
description: Unique identifier of the film
example: 42
minimum: 1
lastUpdate:
type: string
format: date-time
description: When the film record was last updated
example: '2006-02-15T04:03:42Z'
required:
- id
- lastUpdate
FilmFields:
type: object
description: Writable fields for a film (all required fields specified)
properties:
title:
type: string
description: Title of the film
example: 'ACADEMY DINOSAUR'
maxLength: 255
description:
type: string
description: Short synopsis of the film
example: 'An Epic Drama of a Feminist And a Mad Scientist'
maxLength: 255
releaseYear:
type: integer
description: Year the film was released
example: 2006
minimum: 1800
maximum: 2100
rating:
$ref: '#/components/schemas/FilmRating'
length:
type: integer
description: Duration of the film in minutes
example: 86
minimum: 1
language:
$ref: '#/components/schemas/FilmLanguage'
originalLanguage:
$ref: '#/components/schemas/FilmLanguage'
rentalDuration:
type: integer
description: Length of the rental period in days
example: 3
minimum: 0
rentalRate:
type: number
format: double
multipleOf: 0.01
description: Cost to rent the film for the rental period
example: 4.99
minimum: 0
replacementCost:
type: number
format: double
multipleOf: 0.01
description: Amount charged if the film is not returned or is returned damaged
example: 20.99
minimum: 0
specialFeatures:
type: string
description: Special features available on the film DVD
example: 'Trailers,Deleted Scenes'
maxLength: 255
required:
- title
- language
- rentalDuration
- rentalRate
- replacementCost
FilmLanguage:
type: string
enum: [English, Italian, Japanese, Mandarin, French, German]
description: Language used in the film
example: 'English'
FilmRating:
type: string
enum: [G, PG, PG-13, R, NC-17]
description: Motion Picture Association (MPA) rating
example: 'PG'
FilmListResponse:
description: Standard response where the data property is a Page of Films
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
allOf:
- $ref: "#/components/schemas/Page"
- type: object
properties:
content:
items:
$ref: "#/components/schemas/Film"
type: array
required:
- data
FilmResponse:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
$ref: '#/components/schemas/Film'
required:
- data
Page:
type: object
description: Sublist of a list of objects. It allows to gain information about the position of it in the containing entire list
properties:
content:
default: []
items: {}
type: array
pageable:
$ref: "#/components/schemas/Pageable"
totalElements:
default: 0
description: Total number of items that meet the criteria
example: 10
type: integer
totalPages:
default: 0
description: Total pages of items that meet the criteria
example: 10
type: integer
required:
- content
- pageable
- totalElements
- totalPages
Pageable:
type: object
description: Pagination information
properties:
pageNumber:
description: Current page number (starts from 0)
example: 0
minimum: 0
type: integer
pageSize:
description: Number of items retrieved on this page
example: 10
minimum: 0
type: integer
required:
- pageNumber
- pageSize
ResponseMetadata:
type: object
description: Standard response
properties:
instance:
type: string
description: API endpoint that was called
example: '/api/something'
status:
type: integer
description: HTTP status code
example: 200
minimum: 100
maximum: 599
timestamp:
type: string
format: date-time
description: ISO 8601 timestamp of when the response was generated
example: '2026-01-03T17:11:50.826722328Z'
trace:
type: string
description: Unique trace identifier for debugging purposes. Under unexpected situations it can be '00000000000000000000000000000000'
example: '9482c151-b417-43ff-9dbb-ee12b84e5d99'
required:
- instance
- timestamp
- trace
- status
openapi: 3.0.3
info:
title: Sakila API
version: 1.0.0
description: API for the Sakila sample database - A DVD rental store management system
contact:
name: Pollito
url: https://springboot.pollito.tech/about/about-the-author
servers:
- url: http://localhost:8080/api
description: dev
- url: https://sakila-groovy.pollito.tech/api
description: prod
tags:
- name: Films
description: Endpoints related to films
paths:
/films:
get:
tags:
- Films
operationId: getFilms
summary: Lists films
description: Returns a standard response where the data property is a page of films matching the parameters criteria
parameters:
- name: page
in: query
description: Page number (0-based index)
schema:
type: integer
default: 0
minimum: 0
required: false
- name: size
in: query
description: Number of items per page
schema:
type: integer
default: 10
minimum: 1
maximum: 100
required: false
- name: sort
in: query
description: Sort criteria format `property,asc|desc`. Multiple sort parameters allowed
schema:
type: array
items:
type: string
example: "name,asc"
style: form
explode: true
required: false
responses:
'200':
description: Fetched page of films
content:
application/json:
schema:
$ref: '#/components/schemas/FilmListResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- Films
operationId: createFilm
summary: Creates a new film
description: Creates a new film. Returns a standard response where the data property is the created film
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'201':
description: Created Film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/films/{id}:
get:
tags:
- Films
operationId: getFilm
summary: Finds a film by id
description: Returns a standard response where the data property is the film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'200':
description: Fetched film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
tags:
- Films
operationId: updateFilm
summary: Updates a film
description: Updates the fields of a film. Returns a standard response where the data property is the updated film
parameters:
- $ref: '#/components/parameters/filmId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FilmFields'
responses:
'200':
description: Updated film
content:
application/json:
schema:
$ref: '#/components/schemas/FilmResponse'
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags:
- Films
operationId: deleteFilm
summary: Deletes a film
description: Deletes a film with matching id
parameters:
- $ref: '#/components/parameters/filmId'
responses:
'204':
description: Deleted film. No content is returned
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
parameters:
filmId:
name: id
in: path
description: Film unique identifier
required: true
schema:
type: integer
minimum: 1
schemas:
Error:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
description: Standard error response
properties:
title:
type: string
description: HTTP Reason Phrase
example: 'Not Found'
detail:
type: string
description: Description of the problem
example: "No static resource for request '/'."
required:
- title
Film:
description: A film
allOf:
- $ref: '#/components/schemas/FilmFields'
- type: object
properties:
id:
type: integer
description: Unique identifier of the film
example: 42
minimum: 1
lastUpdate:
type: string
format: date-time
description: When the film record was last updated
example: '2006-02-15T04:03:42Z'
required:
- id
- lastUpdate
FilmFields:
type: object
description: Writable fields for a film (all required fields specified)
properties:
title:
type: string
description: Title of the film
example: 'ACADEMY DINOSAUR'
maxLength: 255
description:
type: string
description: Short synopsis of the film
example: 'An Epic Drama of a Feminist And a Mad Scientist'
maxLength: 255
releaseYear:
type: integer
description: Year the film was released
example: 2006
minimum: 1800
maximum: 2100
rating:
$ref: '#/components/schemas/FilmRating'
length:
type: integer
description: Duration of the film in minutes
example: 86
minimum: 1
language:
$ref: '#/components/schemas/FilmLanguage'
originalLanguage:
$ref: '#/components/schemas/FilmLanguage'
rentalDuration:
type: integer
description: Length of the rental period in days
example: 3
minimum: 0
rentalRate:
type: number
format: double
multipleOf: 0.01
description: Cost to rent the film for the rental period
example: 4.99
minimum: 0
replacementCost:
type: number
format: double
multipleOf: 0.01
description: Amount charged if the film is not returned or is returned damaged
example: 20.99
minimum: 0
specialFeatures:
type: string
description: Special features available on the film DVD
example: 'Trailers,Deleted Scenes'
maxLength: 255
required:
- title
- language
- rentalDuration
- rentalRate
- replacementCost
FilmLanguage:
type: string
enum: [English, Italian, Japanese, Mandarin, French, German]
description: Language used in the film
example: 'English'
FilmRating:
type: string
enum: [G, PG, PG-13, R, NC-17]
description: Motion Picture Association (MPA) rating
example: 'PG'
FilmListResponse:
description: Standard response where the data property is a Page of Films
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
allOf:
- $ref: "#/components/schemas/Page"
- type: object
properties:
content:
items:
$ref: "#/components/schemas/Film"
type: array
required:
- data
FilmResponse:
allOf:
- $ref: '#/components/schemas/ResponseMetadata'
- type: object
properties:
data:
$ref: '#/components/schemas/Film'
required:
- data
Page:
type: object
description: Sublist of a list of objects. It allows to gain information about the position of it in the containing entire list
properties:
content:
default: []
items: {}
type: array
pageable:
$ref: "#/components/schemas/Pageable"
totalElements:
default: 0
description: Total number of items that meet the criteria
example: 10
type: integer
totalPages:
default: 0
description: Total pages of items that meet the criteria
example: 10
type: integer
required:
- content
- pageable
- totalElements
- totalPages
Pageable:
type: object
description: Pagination information
properties:
pageNumber:
description: Current page number (starts from 0)
example: 0
minimum: 0
type: integer
pageSize:
description: Number of items retrieved on this page
example: 10
minimum: 0
type: integer
required:
- pageNumber
- pageSize
ResponseMetadata:
type: object
description: Standard response
properties:
instance:
type: string
description: API endpoint that was called
example: '/api/something'
status:
type: integer
description: HTTP status code
example: 200
minimum: 100
maximum: 599
timestamp:
type: string
format: date-time
description: ISO 8601 timestamp of when the response was generated
example: '2026-01-03T17:11:50.826722328Z'
trace:
type: string
description: Unique trace identifier for debugging purposes. Under unexpected situations it can be '00000000000000000000000000000000'
example: '9482c151-b417-43ff-9dbb-ee12b84e5d99'
required:
- instance
- timestamp
- trace
- status
For better visualization you can copy-paste the OpenAPI Specification YAML file from the codebase into Swagger Editor or use OpenAPI (Swagger) Editor in IntelliJ IDEA.
The envelope pattern
You might be looking at the response examples and thinking, "What's with all that extra fluff? Why wrap the actual film data inside a data object?"
{
"instance": "/api/films/42",
"timestamp": "...",
"trace": "...",
"status": 200,
"data": {
"id": 42,
"title": "ACADEMY DINOSAUR",
// ...
}
}
That is a deliberate choice called the Envelope Pattern. And once you start using it, you won't want to go back.
Think of it like physical mail. Every piece of mail you get, whether it's a birthday card or a bill, comes in an envelope. The envelope has standard information on it: a return address, a destination address, a stamp. The actual message is inside.
Our API responses work the same way. The ResponseMetadata schema is our envelope.
ResponseMetadata:
type: object
properties:
instance:
# ...
status:
# ...
timestamp:
# ...
trace:
# ...
Every single response from the API, whether it's a success (200 OK) or an error (404 Not Found, 500 Internal Server Error, etc.), will have this same top-level structure.
- The actual payload goes inside the
dataproperty for successful responses. - For errors, we swap
datafortitleanddetailso we keep adhering to Problem Details for HTTP APIs.
Why even bother?
Consistency, that's the whole game.
- Client side sanity: The developer building the frontend or mobile app can write one piece of code to handle all API responses. They always know where to find the
traceID to show a user reporting a bug. They can build a generic error handler that always looks fortitleanddetail. They aren't guessing whether the payload is the object itself, an array, or some weird error shape. - Future proofing: What if we need to add some other metadata? No problem. We just add a new field to the envelope level. The
datapart remains untouched, and we don't break the client's parsing logic for the actual film data. - It just looks professional: An API with a consistent response structure feels solid and well-thought-out. An inconsistent one feels amateurish and is a pain to work with.
Pagination
TODO: explain pagination
With the OpenAPI specification and the envelope pattern in place, the API contract is fully defined. The next step is to generate Spring Boot code from this spec so the implementation matches the contract exactly.