When the spec and the code drift apart
You spend three hours writing an OpenAPI specification. You define every endpoint, every request body, every error response. Then you open your editor and start typing http.HandleFunc. Two weeks later, a frontend developer complains that the user_id field is missing from the JSON response. You check the spec. It is there. You check your Go struct. It has a lowercase userID. The specification and the implementation have drifted. This happens to almost every team that maintains a REST API by hand. oapi-codegen solves the drift by treating your OpenAPI file as the single source of truth. It reads the specification and emits the Go types, HTTP handlers, and client code that match it exactly.
How the generator translates contracts
Think of oapi-codegen as a translator that speaks OpenAPI fluently and writes idiomatic Go. You give it a YAML or JSON document describing your API. It parses the paths, schemas, and parameters. It outputs Go structs for your models, an interface that defines every route, and a router that wires them together. You only write the business logic. The tool handles the boilerplate of parsing query strings, validating JSON payloads, and routing requests to the correct function. The compiler enforces the contract. If your implementation misses a method or returns the wrong type, the build fails before the code ever reaches production.
The generator follows a strict mapping rule. JSON schemas become Go structs. Snake case field names become CamelCase with json struct tags. Path parameters become function arguments. Query parameters become arguments. Request bodies become structs. Response codes become return values or writer calls. The tool also generates a client that knows how to construct URLs, set headers, and marshal request bodies. You accept the generated interface and return your own structs. This follows the standard Go mantra: accept interfaces, return structs. The generated code expects an interface, and you provide a concrete implementation that holds your database connections and business rules.
Keep your generated files out of version control if your team prefers to treat them as build artifacts, or commit them if you want CI to catch spec changes early. Either way, never edit the generated file by hand. Trust the tool to keep the contract intact.
The fastest path to a working server
Here is the quickest way to see the generator in action. Install the CLI and run it against a basic specification file.
# Install the latest version directly from the repository
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
# Generate types, client, and server stubs into a single file
oapi-codegen -generate types,client,server -package api openapi.yaml > api/generated.go
The command reads openapi.yaml, compiles the schema into Go code, and writes the result to api/generated.go. The -package flag sets the import path name, and -generate tells the tool which components to emit. You can now implement the generated interface.
// serverImpl satisfies the generated ServerInterface
type serverImpl struct{}
// GetUsers handles the GET /users endpoint
func (s *serverImpl) GetUsers(w http.ResponseWriter, r *http.Request) {
// Return a hardcoded list for demonstration
users := []User{{ID: 1, Name: "Alice"}}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(users)
}
// main starts the HTTP server with the generated router
func main() {
server := &serverImpl{}
// HandlerFromMux wires the interface to the standard library mux
handler := api.HandlerFromMux(server)
http.ListenAndServe(":8080", handler)
}
The generated code expects a struct that implements ServerInterface. Each endpoint becomes a method on that interface. The router matches incoming HTTP requests to the correct method based on the path and verb. You pass your implementation to HandlerFromMux, and the standard library handles the rest. The receiver name s matches the type abbreviation, which is the standard Go convention for method receivers. Keep it to one or two letters.
Run gofmt on the generated file before committing it. Most editors run it automatically on save, but the generator sometimes outputs formatting that differs from your editor defaults. Let the tool decide the indentation. Argue logic, not formatting.
What happens under the hood
When you run the generator, it walks through the OpenAPI document node by node. It maps JSON schemas to Go structs, converting field names and attaching struct tags. It reads the paths section and creates a method signature for every route. The tool also generates a client that knows how to construct URLs, set headers, and marshal request bodies. At compile time, Go checks your struct against the generated interface. If you rename a method or change a parameter type, the compiler rejects the program with cannot use server (type *serverImpl) as type api.ServerInterface in argument. The contract is enforced by the type system, not by runtime checks.
The generated router uses a trie-based path matcher under the hood. It compiles the OpenAPI paths into a lookup structure that resolves routes in constant time. When a request arrives, the router extracts path parameters, parses query strings, and decodes the request body into the generated struct. It then calls your method. If your method writes to the response writer, the router steps aside. If you use strict mode, the router takes over serialization and status code writing. This separation keeps your handlers focused on business logic instead of HTTP plumbing.
Always pass context.Context as the first parameter to any function that performs I/O. The generated strict handlers already include it. Functions that take a context should respect cancellation and deadlines. This convention keeps long-running operations from blocking the server when a client disconnects.
Configuring for production workflows
Real projects rarely use a single command. You will want to split the output into separate files, add custom headers, and control which components get generated. A YAML configuration file gives you that control.
# Configuration for oapi-codegen
package: api
output: api/generated.go
generate:
types: true
client: true
server: true
strict-server: true
compatibility:
skip-prune: true
The strict-server flag changes the generated interface. Instead of accepting http.ResponseWriter and *http.Request, each method receives a context, a request struct, and returns a response struct or an error. This removes the manual JSON encoding and status code writing from your handlers. The skip-prune option prevents the tool from deleting schemas that are not directly referenced, which is useful when you define reusable error types.
# Run the generator using the configuration file
oapi-codegen -config .oapi-codegen.yaml openapi.yaml
With strict mode enabled, your implementation looks cleaner. The router handles serialization and status codes automatically.
// GetUsers handles the GET /users endpoint with strict routing
func (s *serverImpl) GetUsers(ctx context.Context, request api.GetUsersRequestObject) (api.GetUsersResponseObject, error) {
// Extract pagination from the generated request object
limit := request.Limit
offset := request.Offset
// Fetch users from the database using the provided context
users, err := s.db.FindUsers(ctx, limit, offset)
if err != nil {
// Return a typed error that the router converts to JSON
return api.GetUsers500JSONResponse{Message: "database error"}, nil
}
// Return the success response with the 200 status code
return api.GetUsers200JSONResponse{Users: users}, nil
}
The strict router matches the return type to the correct HTTP status code. If you return a 500JSONResponse, it writes 500 Internal Server Error and marshals the struct. You never touch w.WriteHeader or json.Marshal in your handlers. The if err != nil { return err } pattern remains verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Do not hide errors behind silent returns.
Integrate the generator into your build pipeline using go generate. Add a comment at the top of your main.go or a dedicated gen.go file: //go:generate oapi-codegen -config .oapi-codegen.yaml openapi.yaml. Run go generate ./... before every build. This ensures the generated code stays in sync with the API spec. If your spec changes, simply re-run the command to update the Go files.
Treat the generated package as read-only infrastructure. Wrap the value or change the design instead of fighting the type system.
Where things break and how to fix them
Code generation introduces a new set of failure modes. The most common mistake is editing the generated file by hand. The next time you run the tool, your changes disappear. Keep all custom logic in separate files that import the generated package. Another trap is mismatched types. If your OpenAPI spec defines a field as type: string but your Go code expects an integer, the compiler rejects the program with cannot use x (type string) as type int in assignment. Fix the spec, not the Go code. The spec is the contract.
Strict mode can also surprise new users. The generated interface requires you to return specific response structs for every documented status code. If you forget to return the 404 variant, the compiler complains with GetUsers returns 1 values but expects 2. You must handle every documented response path. This feels verbose at first, but it prevents silent failures where a missing error case returns an empty 200 OK response.
Goroutine leaks happen when you spawn background tasks that depend on request-scoped channels. The generated router does not manage goroutine lifecycles for you. If you start a worker in a handler, attach it to the request context and cancel it when the response finishes. The worst goroutine bug is the one that never logs. Always tie background work to the incoming context.
Public names start with a capital letter. Private start lowercase. No keywords like public or private. The generator follows this rule automatically. If you need to discard a return value intentionally, use _. result, _ := ... says you considered the second return value and chose to drop it. Use it sparingly with errors. Never discard an error unless you have a documented reason to ignore it.
Do not pass a *string. Strings are already cheap to pass by value. The generator uses value types for simple fields and pointers only for optional fields. Trust the generated signatures. They follow the standard library conventions.
Choosing the right routing strategy
Use oapi-codegen when you maintain a public REST API and need the client and server to stay perfectly synchronized. Use manual net/http routing when you are building a small internal tool and want zero dependencies. Use a lightweight router like chi or gin when you need middleware composition and flexible routing patterns without strict contract enforcement. Use grpc when you are building internal microservices that prioritize performance and strongly typed protobuf contracts over browser compatibility.
Context is plumbing. Run it through every long-lived call site.