The contract-first trap
You spend three days building a REST endpoint in Go. The JSON parsing works. The database query is fast. You push it to staging, and the frontend team replies with a single message: your response shape does not match the contract they built against. You rewrite the struct tags, update the documentation, and deploy again. Two weeks later, the mismatch happens on a different endpoint. Writing API contracts by hand and keeping them synchronized with Go code is a maintenance tax that compounds quickly.
What the generator actually builds
go-swagger flips the workflow. You write the OpenAPI specification first. The specification becomes the single source of truth. The CLI reads the YAML or JSON file and generates the Go scaffolding that enforces that contract at compile time. Think of it like a building permit that automatically drafts the electrical and plumbing diagrams. You still wire the outlets and pipe the water, but the generator guarantees the layout matches the approved plans. The tool creates routers, middleware chains, model structs, and handler interfaces. Your job shrinks to implementing the business logic inside those interfaces.
The generated code does not replace your application. It replaces the boilerplate that usually causes drift between documentation and implementation. You get type-safe request parsing, automatic validation, a configured router, and a middleware stack. You keep full control over database calls, external service clients, and business rules.
Keep the spec as the source of truth. Regenerate when the contract changes.
Generating the skeleton
Start with a specification that describes a single endpoint. Keep it small so you can see exactly what the generator produces.
Here is a minimal OpenAPI 2.0 file that defines a health check endpoint:
swagger: "2.0"
info:
title: "Minimal API"
version: "1.0.0"
paths:
/health:
get:
operationId: "getHealth"
responses:
200:
description: "Service is running"
Run the CLI to generate the server scaffolding:
# installs the latest release of the swagger CLI
go install github.com/go-swagger/go-swagger/cmd/swagger@latest
# reads the spec and writes Go packages into the current directory
swagger generate server -f swagger.yaml
The command creates a directory structure that mirrors the specification. You will see a models package for request and response types, a restapi package for routing and middleware, and an operations subpackage containing one interface per endpoint. The cmd/api-server/main.go file ties everything together. Open it and you will find a ConfigureAPI function that binds the generated router to the handler interfaces.
The generator does not guess your business logic. It leaves the handler implementations empty and expects you to fill them in. This separation keeps the generated code stable. You can regenerate the scaffolding later without overwriting your custom logic, as long as you keep your implementations in separate files or use the --skip-models and --skip-router flags when appropriate.
Let the spec drive the structure. Write your logic in the gaps.
Wiring a real handler
The generated operations package contains an interface for every endpoint. For the health check above, it creates a GetHealthHandler interface with a single method. You implement that interface in your own code and pass it to the generated configuration.
Here is a complete handler implementation that respects Go conventions:
package handlers
import (
"context"
"net/http"
)
// HealthHandler implements the generated GetHealthHandler interface
type HealthHandler struct {
// dbClient holds a reference to the database pool
dbClient *DBPool
}
// GetHealth handles the /health GET request
func (h *HealthHandler) GetHealth(ctx context.Context) (*models.HealthResponse, error) {
// context carries cancellation and deadline from the router
if err := h.dbClient.Ping(ctx); err != nil {
// return a typed error that the middleware can translate to HTTP 503
return nil, err
}
// build the response struct that matches the spec definition
return &models.HealthResponse{Status: "ok"}, nil
}
The receiver name is a single letter matching the type initial. The method signature matches the generated interface exactly. The context.Context parameter is first, following the standard library convention. The function returns a typed response and an error. The generated middleware stack will catch the error and convert it to the appropriate HTTP status code.
Wire the handler into the generated server in main.go:
package main
import (
"log"
"net/http"
"myapi/restapi"
"myapi/restapi/operations"
"myapi/handlers"
)
func main() {
// create the handler with your dependencies
h := &handlers.HealthHandler{dbClient: NewPool()}
// bind the handler to the generated operations struct
api := operations.NewMyAPIAPI(nil)
api.GetHealthHandler = h
// start the HTTP server on port 8080
server := &http.Server{Addr: ":8080", Handler: api}
log.Fatal(server.ListenAndServe())
}
The operations.NewMyAPIAPI call creates the router and middleware chain. Assigning api.GetHealthHandler = h plugs your implementation into the generated skeleton. The server starts listening and routes incoming requests through the validation layer before calling your method.
Run the server and test the endpoint:
go run cmd/api-server/main.go
# output:
# listening on [::]:8080
The generated code handles JSON marshaling, parameter extraction, and validation. You only write the database call and the response construction.
Trust the generated router. Keep your handlers thin.
When the generator fights back
Code generation introduces a new class of friction. The compiler will reject your code if your implementation does not match the generated interface exactly. If you rename a field in the spec and forget to update the response struct, you get cannot use &models.HealthResponse literal (type *models.HealthResponse) as type *models.HealthResponse in return argument because the old and new model types are technically different Go types. If you drop the context.Context parameter, the compiler complains with cannot use h (type *HealthHandler) as type operations.GetHealthHandler in assignment.
The generator also enforces strict validation rules. If your spec marks a field as required but your handler returns a nil pointer, the middleware returns a 500 error before your code even runs. This is intentional. The contract is enforced at the boundary, not buried inside business logic.
You will occasionally need to override generated files. The CLI supports --template-dir and --additional-initialism flags, but the safest path is to keep custom code in separate packages. Regenerate into a clean directory, copy your handler files back, and let gofmt standardize the indentation. Do not fight the formatter. Let the tool decide spacing and brace placement.
Error handling follows the standard Go pattern. Write if err != nil { return nil, err } on every call that can fail. The verbosity is deliberate. It makes the unhappy path visible to anyone reading the function. The generated middleware will inspect the error type and map it to HTTP status codes automatically.
Goroutine leaks happen when a handler spawns a background task that waits on a channel the handler never closes. Always pass the request context to spawned goroutines and cancel it when the response is sent. The worst concurrency bug is the one that silently holds memory until the next deployment.
Keep the spec authoritative. Regenerate often. Never edit generated files by hand.
Picking the right tool
Use go-swagger when you need a strict contract-first workflow and want compile-time guarantees that your Go code matches the OpenAPI specification. Use a lightweight router like chi or gin when you prefer code-first development and are comfortable maintaining documentation separately. Use swag when you want to generate OpenAPI specs from Go doc comments without writing YAML by hand. Use protobuf and grpc-go when you are building internal microservices that prioritize binary efficiency and strict schema evolution over public REST contracts.
The right choice depends on who consumes your API and how fast the contract changes. Public APIs benefit from the discipline of contract-first generation. Internal services often move faster with code-first routing.
Match the tool to the audience. Let the contract dictate the boundary.