Image of a circular wall clock with golden hands on a white background, overlaid with a blurred branch of green leaves.
Photo by @surjasendas on Unsplash

When it comes to back-end applications, exposing documentation is essential — it's fundamental for a better understanding of how endpoints work and what they can or cannot do.

We won't focus on implementation details here, so let's assume your application is minimally configured and has at least one API route exposed.

Specification

The first thing we need to consider is the definition of rules and behaviors for a given endpoint. This includes:

  1. Supported HTTP verb;
  2. Input parameters;
  3. Success and/or error response models;
  4. Authentication method;
  5. Permissions when available.

Now, you might be wondering: "Ok, we need to gather a lot of definitions for an API — how do we do that in practice?" (If you haven't asked yourself that yet, now's the time...) The answer is: Open API!

What is OpenAPI?

OpenAPI Specification (OAS) is a set of concepts and standards for defining the structure and behavior of APIs over the HTTP protocol. The core idea is to have an agnostic way to describe these rules, typically defined in a YAML or JSON file.

Key highlights when using OAS specifications:

  • Easier identification of services and API contracts through standardization.
  • Ability to generate code and tests based on the file definitions.
  • Infrastructure automation.

Using it in our code

Each language and framework will have its own way of implementing OpenAPI specifications, but in general they all follow the same approach: creating a file with the rules for every resource exposed by the API.

Here's an example:

openapi: 3.0.0
info:
  title: Sample API
  description: A sample API to demonstrate OpenAPI specification
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
    description: Main production server
tags:
  - name: Users
    description: Operations related to users
  - name: Products
    description: Operations related to products
paths:
  /users:
    get:
      summary: Get all users
      tags:
        - Users
      responses:
        '200':
          description: A list of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '500':
          description: Internal server error
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          example: 12345
        name:
          type: string
          example: John Doe
        email:
          type: string
          example: john.doe@example.com
        createdAt:
          type: string
          format: date-time
          example: 2023-10-01T12:00:00Z

In the example above, there's only one endpoint available: Get /users. You could spend hours writing each one by hand, but there are faster and more efficient ways to do this.

Automating the process

There are many ways to automate the generation of the specification file. We'll use a strategy that I personally find efficient while still giving us control and transparency over the code.

We'll use a NodeJS application as an example, along with two libraries to handle the automation process:

The setup process is pretty straightforward. First, we need to create a file in the project root — I named it swagger.ts:

// swagger.ts

import swaggerJSDoc from 'swagger-jsdoc'

const swaggerDefinition = {
  openapi: '3.0.0',
  info: {
    title: 'Gen AI API',
    version: '1.0.0',
    description:
      'This is an Express service that provides authorization functionality and includes gen-AI features using RAG, Redis, Postgres, and Langchain.',
  },
  servers: [
    {
      url: 'http:localhost:3000',
    },
  ],
  tags: [
    {
      name: 'Auth',
      description: 'Endpoints related to authentication',
    },
    {
      name: 'Users',
      description: 'Endpoints related to users management',
    },
    {
      name: 'AI',
      description: 'Endpoints related to gen-AI features',
    },
  ],
  components: {
    securitySchemes: {
      bearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
      },
    },
  },
}

const options = {
  swaggerDefinition,
  apis: ['./src/modules/**/application/routes/*.ts'],
}

const swaggerSpec = swaggerJSDoc(options)

export default swaggerSpec

In the code above we define some metadata for the general API description. Worth noting the tags field — this array of objects lets you add sections to group endpoints by type.

If you want to better understand the allowed values for the specification file, check out the official documentation.

Now, for each route in the application, we'll add a comment with the specification for that specific route. This is important because the application will read the route files indicated in the apis field shown in the swagger.ts file from the previous example.

// route.ts

import express from 'express'

const router = express.Router()

/**
 * @swagger
 * /status:
 *   get:
 *     summary: Returns a message to validade if API server is running
 *     responses:
 *       200:
 *         description: A successful response
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 message:
 *                   type: string
 *                   example: Systems up and running!
 */
router.get('/', (_, res) => {
  res.status(200).send({ message: 'Systems up and running!' })
})

export default router

By adding the @swagger annotation at the start of the comment, we're indicating that this block should be used to include that route in the documentation — which will eventually be merged with the rest of the content.

Now let's expose an endpoint to view the documentation. Go to your application's app.ts file (or equivalent) and add:

import express from 'express'
import swaggerUi from 'swagger-ui-express'

const app = express()
const port = process.env['PORT'] || config.port

...
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
...
app.listen(port, () => {
	console.log(`Server listening to http://localhost:${port}`)
})

Simple as that — once the application is running, the documentation will be available at the /docs endpoint.

Extra: Automating the automation using AI agents

So far so good, but there's one more thing: we still need a bit of manual effort to write the comments for each endpoint. Thanks to existing generative AI models, we can simplify this process even further by asking the AI to generate the comment in the format we need.

Check out the prompt below:

Generate the JSDocs comments using OpenAPI for the following API specs:

"""
endpoint name: /users/{id}
path parameter type: string
method: PUT
request body: {
  "name": "John Doe",	"email": "john.doe@email.com",
}
summary: Update a user by given id and return it
200_response: {
	"success": true,
	"data": [
    {
      "id": "367b2539-bef4-412b-b94d-c9d2178dcdaa",
      "name": "John Doe",
      "email": "john.doe@gmail.com",
      "createdAt": "2024-09-30T21:04:18.656Z"
    }
  ]
}
400_response: {
  "success": false,
  "error": {
    "message": [
      "Invalid email address",
			"name field is required"
    ]
  }
}
headers: {
  "Content-Type": "application/json",
  "Authorization": "Bearer <token>"
}
"""

More context: I'm using `swagger-ui-express` and `swagger-jsdoc` libraries to setup the Swagger documentation.
Give me only the response, without explanations.

Here we're providing all the necessary information for this endpoint along with response examples, and also specifying which libraries we're using to generate the documentation. These extra details are important because they give the model more context, resulting in more accurate responses.

Check out the agent's response to this prompt:

/**
 * @swagger
 * /users/{id}:
 *   put:
 *     summary: Update a user by given id and return it
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *                 example: John Doe
 *               email:
 *                 type: string
 *                 example: john.doe@email.com
 *     responses:
 *       200:
 *         description: A successful response
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 data:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: string
 *                       example: 367b2539-bef4-412b-b94d-c9d2178dcdaa
 *                     name:
 *                       type: string
 *                       example: John Doe
 *                     email:
 *                       type: string
 *                       example: john.doe@gmail.com
 *                     createdAt:
 *                       type: string
 *                       format: date-time
 *                       example: 2024-09-30T21:04:18.656Z
 *       400:
 *         description: Invalid request
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: false
 *                 error:
 *                   type: object
 *                   properties:
 *                     message:
 *                       type: array
 *                       items:
 *                         type: string
 *                       example: ["Invalid email address", "name field is required"]
 *     security:
 *       - bearerAuth: []
 */

All we need to do now is review and make any necessary adjustments to the generated comment and paste it right above the API endpoint definition.

Wrapping up

In this article we learned a bit more about the OpenAPI standard and how we can automate the process of documenting an API, as well as how to use AI agents to make that process even easier.

I hope this content brought you some new knowledge or even some insights for new ideas — thanks for taking the time to read it, and I'll see you in the next one! :)

Check out below how the documentation generation turned out for my project, and if you'd like to understand it better and look at the implementation — just follow this link.

Image of the documentation generated by Swagger UI