Crafting Peaceful REST API

Sebastian Pawlaczyk
DevBulls

--

When embarking on the journey of REST API development, the initial phase significantly impacts subsequent stages, such as implementation and maintenance. In this article, I delve into common pitfalls in the API design process and propose solutions through a practical example, emphasizing the use of OpenAPI.

First pain: Lack of documentation

The absence of documentation in projects is a recurring challenge. Many developers can relate to the frustration of encountering projects with undocumented APIs. Starting implementation directly by defining endpoints without a prior specification leads to several issues:

  • The lack of a strict JSON format is prone to overlooked changes. It can result in mismatches between the data provided by the client and what is expected by the server.

Body provided by the client:

{
"name": "foo",
"num": 1
}

Body expected by the server:

{
"name": "foo",
"number": 1
}
  • Integrating with new services without initial documentation becomes challenging. It often leads to time-consuming and error-prone adjustments.

Second Pain: Post-Implementation Documentation

Assuming documentation is created after implementation, tools like Swaggo(https://github.com/swaggo/swag) convert code annotations into Swagger Documentation(https://swagger.io/). While this approach addresses some documentation issues, it still has limitations:

  • annotations may not always reflect the current state of the code, leading to discrepancies
  • integration with the API must wait until after implementation, delaying potential early-stage integrations
  • manual validation of basic requests is necessary, as automated checks are not in place

Example:

package handler

// @Description create new asset
// @Tags Assets
// @Accept json
// @Produce json
// @Param message body NewAsset true "Asset request body"
// @Success 201 {object} Asset
// @Failure 400 {object} HTTPError
// @Failure 500 {object} HTTPError
// @Router /assets [post]
func postAssets() {}

Solution: OpenAPI with stub generator

OpenAPI provides a robust solution to these challenges. It enables developers to define APIs upfront, ensuring clear and consistent documentation from the outset. This approach not only facilitates better integration and development workflows but also ensures that the API’s contract is clear and adhered to throughout the development process.

Benefits of OpenAPI solution

  1. Early Validation: With OpenAPI, endpoints are defined and validated before implementation, reducing the risk of mismatches and integration issues.
  2. Client-Server Agreement: It establishes a clear contract between client and server, ensuring that both parties agree on the API’s structure and behavior before coding begins.
  3. Automated Tooling Support: OpenAPI supports a wide range of tools for generating client libraries, server stubs, and documentation, streamlining the development process.

Incorporating OpenAPI in Your workflow

  1. Define Your API: Start by outlining your API using the OpenAPI Specification (OAS). This includes endpoints, request/response structures, and authentication mechanism.
  2. Validate and Iterate: Use tools like Swagger Editor to validate your OAS definition and iterate on your API design.
  3. Generate Code and Documentation: Utilize tools like Swagger Codegen or OpenAPI Generator to generate server stubs, client libraries, and up-to-date documentation.

Let’s code it

Step 1: Define your API

Create yaml file, which is a REST API contract

# /api/openapi.yaml
openapi: 3.0.0
info:
title: REST in peace API 1.0
description: |-
DevBulls microservice for financial assets
contact:
email: sebastian.pawlaczyk@devbulls.com
version: 1.0.0
externalDocs:
description: Read about REST API
url: https://devbulls.com
servers:
- url: https://host-example.com
tags:
- name: assets
description: Everything about your Assets
paths:
/assets:
post:
tags:
- assets
summary: Create a new asset
description: Create a new asset
operationId: postAssets
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewAsset'
responses:
'200':
description: Asset created successfully
content:
application/json:
schema:
required:
- assets
properties:
assets:
type: array
items:
$ref: '#/components/schemas/Asset'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
get:
tags:
- assets
summary: Get list of assets
description: Get list of assets
operationId: getAssets
parameters:
- name: type
in: query
required: false
description: Search by specific type
schema:
type: string
responses:
'200':
description: List of the assets
content:
application/json:
schema:
required:
- assets
properties:
assets:
type: array
items:
$ref: '#/components/schemas/Asset'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/assets/{assetID}:
get:
tags:
- assets
summary: Get a specific asset
description: Get a specific
operationId: getAssetsByID
parameters:
- name: assetID
in: path
required: true
description: The UUID of the asset.
schema:
type: string
responses:
'200':
description: Asset entity
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
tags:
- assets
summary: Update an asset entirely
description: Update an asset entirely
operationId: putAssets
parameters:
- name: assetID
in: path
required: true
description: The UUID of the asset.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewAsset'
responses:
'200':
description: Asset updated successfully
content:
application/json:
schema:
required:
- assets
properties:
assets:
type: array
items:
$ref: '#/components/schemas/Asset'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
tags:
- assets
summary: Delete asset
description: Delete asset
operationId: deleteAssets
parameters:
- name: assetID
in: path
required: true
description: The UUID of the asset.
schema:
type: string
responses:
'204':
description: Asset deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
description: Error code
message:
type: string
description: Error message
Asset:
type: object
required:
- id
- name
- type
properties:
id:
type: string
description: Identifier of the asset
format: uuid
name:
type: string
description: Name of the asset
type:
description: Type of the asset
type: string
enum:
- Bond
- Stock
- Cryptocurrency
symbol:
description: Symbol of the asset
type: string
currency:
description: Currency
type: string
enum:
- USD
- EUR
- GBD
description:
description: Description of the asset
type: string
NewAsset:
type: object
required:
- name
- type
- symbol
- sector
- currency
- description
properties:
name:
type: string
description: Name of the asset
type:
description: Type of the asset
type: string
enum:
- Bond
- Stock
- Cryptocurrency
symbol:
description: Symbol of the asset
type: string
currency:
description: Currency
type: string
enum:
- USD
- EUR
- GBD
description:
description: Description of the asset
type: string

Step 2: Validate contract

You can use online editors like: https://editor.swagger.io, or IDE plugins to verify your work.

Step 3: Generate code

Now it is time to generate Golang code stubs based on designed contract.

Install server code generator
My choice is an oapi-codegen, you can find install instructions here: https://github.com/deepmap/oapi-codegen

Define config files
Create two config files for server and models. In example, I use gin framework for golang server, but you have various options.

# /api/server.cfg.yml
package: assets_api
output: api/assets-server.gen.go
generate:
embedded-spec: true
strict-server: true
gin-server: true
# /api/types.cfg.yml
package: assets_api
output: api/assets-types.gen.go
generate:
models: true

Run generator

oapi-codegen --config api/server.cfg.yml api/openapi.yaml
oapi-codegen --config api/types.cfg.yml api/openapi.yaml

As a result two files are generated:

  • api/assets-server.gen.go — services for which you need to implement business logic
  • api/assets-types.gen.go — models for requests and responses

You are ready to code.

--

--