At this stage, our DevOps API was already fully operational and deployed in Kubernetes (From DevOps API to Self-Documented Platform: Building and Documenting a Go API with Swagger in Kubernetes – Part 1). It was integrated with GitLab, capable of retrieving semantic version tags, exposing multiple endpoints, and accessible via a Gateway API backed by Traefik.

However, despite being functional, the API had a critical limitation: it lacked discoverability and self-documentation.

Every consumer needed prior knowledge of:

  • available endpoints
  • required query parameters
  • expected responses

This is where Swagger (OpenAPI) becomes essential.


Why Swagger?

Swagger transforms an API from a black box into a self-describing contract.

Instead of relying on external documentation or tribal knowledge, Swagger provides:

  • interactive documentation
  • real-time request execution
  • standardized API contracts
  • automatic synchronization with code

Given that our API was already in production, the goal was not to redesign it — but to incrementally enrich it with documentation, without breaking its existing structure.


Step 1: Installing Swagger Tooling (swag)

The Go ecosystem provides an excellent tool called swag, which generates OpenAPI specifications directly from code annotations.

We installed it using:

go install github.com/swaggo/swag/cmd/swag@latest

Step 2: Understanding the Go Module Name

Before integrating Swagger into the application, it is essential to understand how Go modules work.

Swagger generates a docs package, which must be imported into the application. This import depends on the module name defined in go.mod.

To identify the correct import path:

cat go.mod

Example:

module devops-api

This means the Swagger docs must be imported as:

package main

import (
	_ "devops-api/docs"

	"github.com/swaggo/http-swagger"
)

If the module were defined differently, for example:

module github.com/devops-db/devops-api

Then the import would need to reflect that:

_ "github.com/devops-db/devops-api/docs"

This step is critical — without it, Swagger will not be registered at runtime.


Step 3: Adding Swagger Annotations to Handlers

Rather than rewriting the API, we enhanced existing handlers with Swagger annotations.

For example, the /tags endpoint — one of the most important endpoints in our API — was documented as follows:

// @Summary Get repository tags
// @Description Get semver tags from a GitLab repository
// @Tags tags
// @Produce json
// @Param repo query string true "Repository path (e.g. services/reporting)"
// @Param sort query string false "Sort order (date|version)"
// @Param limit query int false "Limit number of results"
// @Success 200 {array} string
// @Failure 400 {string} string "Invalid parameters"
// @Failure 500 {string} string "Internal error"
// @Router /tags [get]
func tagsHandler(w http.ResponseWriter, r *http.Request) {

We applied similar annotations across all endpoints:

  • /health
  • /databases
  • /schemas
  • /blueprints/platforms
  • /blueprints/environments
  • /blueprints/services

This approach ensured that:

  • no logic was changed
  • documentation stayed close to implementation
  • future updates remain synchronized

Step 4: Generating Swagger Documentation

With annotations in place, we generated the Swagger specification:

swag init

This command creates a new directory:

/docs
  docs.go
  swagger.json
  swagger.yaml

The docs.go file contains the embedded Swagger specification and must be compiled with the application.


Step 5: Serving Swagger UI

To expose the Swagger UI, we added the following dependency:

go get github.com/swaggo/http-swagger

Then, in the main application:

package main

import (
	"database/sql"
	"fmt"
	"net/http"
	"os"

	_ "github.com/lib/pq"

	_ "devops-api/docs"

	"github.com/swaggo/http-swagger"
)

And registered the Swagger route:

http.Handle("/swagger/", httpSwagger.WrapHandler)

Once deployed, the Swagger UI became available at:

http://devops-api.devops-db.internal/swagger/index.html

Step 6: Rebuilding and Deploying

Since Swagger generates code that becomes part of the application, we needed to:

  1. Run swag init
  2. Commit the generated /docs directory
  3. Rebuild the Docker image
  4. Push to Nexus
  5. Redeploy via Helm

This resulted in version 1.1.0 of our devops-api.


Step 7: Verifying the Result

Once deployed, the Swagger UI provided:

  • categorized endpoints (health, tags, databases, schemas, blueprints)
  • parameter input fields
  • real-time execution
  • actual responses from GitLab and PostgreSQL

For example, executing:

GET /tags?repo=services/reporting&sort=date

returns:

[
  "v0.1.0",
  "v0.1.1",
  ...
  "v1.3.0"
]

This confirms that the documentation is not static — it is directly connected to the live system.


Key Takeaways

Adding Swagger was not about generating documentation — it was about turning the API into a consumable interface.

The most important aspects of this implementation were:

  • keeping documentation close to the code
  • avoiding structural changes
  • ensuring compatibility with existing deployments
  • leveraging real data instead of mocks

With minimal changes, the DevOps API evolved from a functional service into a fully self-documented platform, ready to be consumed by developers, pipelines, and automation tools.


Conclusion

Swagger is often introduced early in projects, but integrating it into an existing, production-ready system provides a much stronger perspective.

In this case, the process demonstrated that:

A well-designed API does not need to be rewritten to be documented — it only needs to be described correctly.

With version 1.1.0, the DevOps API now exposes not only its functionality, but also its contract — making it significantly more powerful and easier to adopt.