Server Generation¶
oapi-codegen can generate server-side handler code with a clean service interface pattern. This separates HTTP handling from business logic, making your code easier to test and maintain.
Overview¶
The server generation feature creates:
- Service Interface - A Go interface defining your business logic methods
- HTTP Adapter - Generated code that handles HTTP parsing and calls your service
- Router Registration - Framework-specific code to register routes
- Scaffold Files - One-time generated files for your implementation (
service.go,middleware.go) - Server Main (optional) - A runnable
main.gowith middleware setup
Quick Start¶
Create a configuration file:
# yaml-language-server: $schema=https://raw.githubusercontent.com/doordash-oss/oapi-codegen-dd/HEAD/configuration-schema.json
package: api
output:
directory: api
generate:
handler:
kind: chi # or echo, gin, fiber, std-http, etc.
service: {} # Generate service.go scaffold
middleware: {} # Generate middleware.go scaffold
server:
directory: server
handler-package: github.com/myorg/myapi/api
Run the generator:
go run github.com/doordash-oss/oapi-codegen-dd/v3/cmd/oapi-codegen -config cfg.yaml spec.yaml
This generates:
api/
├── gen.go # Generated types, adapter, router (always regenerated)
├── service.go # Your service implementation (scaffold, edit this)
└── middleware.go # Custom middleware (scaffold, edit this)
server/
└── main.go # Runnable server (scaffold, edit this)
Supported Frameworks¶
| Framework | Kind | Path Params | Notes |
|---|---|---|---|
| chi | chi |
chi.URLParam(r, "id") |
Lightweight, idiomatic |
| Echo | echo |
c.Param("id") |
Feature-rich, middleware ecosystem |
| Gin | gin |
c.Param("id") |
High performance, popular |
| Fiber | fiber |
c.Params("id") |
Express-inspired, fasthttp-based |
| std-http | std-http |
r.PathValue("id") |
Go 1.22+ standard library |
| Beego | beego |
c.Ctx.Input.Param(":id") |
Full-stack framework |
| go-zero | go-zero |
r.PathValue("id") |
Microservice framework |
| Kratos | kratos |
r.PathValue("id") |
Microservice framework |
| Gorilla Mux | gorilla-mux |
mux.Vars(r)["id"] |
Classic router |
| GoFrame | goframe |
r.Get("id") |
Full-stack framework |
| Hertz | hertz |
c.Param("id") |
High-performance from ByteDance |
| Iris | iris |
ctx.Params().Get("id") |
Feature-rich, MVC support |
| fasthttp | fasthttp |
ctx.UserValue("id") |
Zero-allocation HTTP |
Architecture¶
The generated code follows a clean architecture pattern:
┌─────────────────────────────────────────────────────────────┐
│ HTTP Request │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Router (chi/echo/gin/...) │
│ Routes registered by NewRouter() │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ HTTPAdapter │
│ • Parses path/query/body parameters │
│ • Validates request (optional) │
│ • Calls ServiceInterface method │
│ • Writes response │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ServiceInterface │
│ • Pure business logic │
│ • No HTTP concerns │
│ • Easy to test │
└─────────────────────────────────────────────────────────────┘
Configuration Reference¶
generate.handler.kind¶
Required. The router framework to generate for.
generate:
handler:
kind: chi
generate.handler.name¶
Name of the generated service interface. Default: "Service".
generate:
handler:
kind: chi
name: "UserAPI" # Generates UserAPIInterface
generate.handler.models-package¶
When models are generated separately (generate.models: false) into a
different package from the handler, this tells the handler where to find
them. Every model type reference in the generated handler is qualified with
alias, and its import is added automatically - no additional-imports
entry needed.
generate:
models: false # models were generated separately, see below
handler:
kind: chi
models-package:
path: example.com/myapp/models # Go import path
alias: models # optional, defaults to "models"
# models.cfg.yaml - run this first
package: models
output: {directory: models, filename: models.gen.go}
generate: {models: true}
# server.cfg.yaml - run this second
package: server
output: {directory: server, filename: server.gen.go}
generate:
models: false
handler:
kind: chi
models-package: {path: example.com/myapp/models}
service: {}
Both runs must be given the same spec, the same filter, and the same
error-mapping (if any) - the handler run needs to compute the exact same
type names the models run produced, without generating them itself.
generate.handler.handler-package-alias¶
Unrelated to models-package above: this is for when service.go (the
scaffold) is generated into its own package, separate from the handler code
itself. It qualifies handler-owned symbols like ServiceInterface and
<Op>ServiceRequestOptions as seen from service.go.
generate:
handler:
kind: chi
handler-package-alias: server # service.go references server.ServiceInterface
output:
directory: service
package: service # service.go lives here, handler code elsewhere
service: {}
Renamed from models-package-alias
This option used to be called models-package-alias, which was
misleading - it never referred to a package containing models, only to
the package holding the generated handler. models-package-alias still
works as a deprecated fallback.
generate.handler.validation¶
Enable request/response validation in handlers.
generate:
handler:
kind: chi
validation:
request: true # Validate incoming requests
response: true # Validate outgoing responses (for testing)
generate.handler.output¶
Control where scaffold files are written.
generate:
handler:
kind: chi
output:
directory: api # Where to write service.go, middleware.go
package: api # Package name for scaffold files
overwrite: true # Force regenerate scaffold files
generate.handler.service¶
Enable service scaffold generation. Set to {} to generate service.go.
generate:
handler:
kind: chi
service: {} # Generates service.go
Note
Service generation is opt-in. If not specified, service.go will not be generated.
When server is configured, service must also be configured.
generate.handler.middleware¶
Enable middleware scaffold generation. Set to {} to enable.
generate:
handler:
kind: chi
middleware: {} # Generates middleware.go
generate.handler.server¶
Generate a runnable server with middleware setup.
generate:
handler:
kind: chi
server:
directory: server # Output directory
port: 8080 # Server port
timeout: 30 # Request timeout (seconds)
handler-package: github.com/myorg/myapi/api # Import path for handler
Generated Code¶
Service Interface¶
For each operation in your OpenAPI spec, a method is generated on the service interface:
type ServiceInterface interface {
// HealthCheck Health check endpoint
HealthCheck(ctx context.Context) (*HealthCheckResponseData, error)
// CreateUser Create a new user
CreateUser(ctx context.Context, opts *CreateUserServiceRequestOptions) (*CreateUserResponseData, error)
// GetUser Get a user by ID
GetUser(ctx context.Context, opts *GetUserServiceRequestOptions) (*GetUserResponseData, error)
}
Request Options¶
Operations with parameters receive a *<Operation>ServiceRequestOptions struct:
type CreateUserServiceRequestOptions struct {
RawRequest *http.Request // Original HTTP request
Body *CreateUserRequest // Parsed request body
}
type GetUserServiceRequestOptions struct {
RawRequest *http.Request // Original HTTP request
PathParams *GetUserPathParams // Path parameters
Query *GetUserQuery // Query parameters
}
Form-Encoded Requests¶
When your OpenAPI spec defines application/x-www-form-urlencoded as the request content type,
the generated adapter automatically parses form data into your typed request body.
The adapter supports deepObject encoding as defined in OpenAPI 3.0, which allows nested objects and arrays in form data:
# Simple key-value pairs
name=John&age=30&active=true
# Nested objects (deepObject style)
address[city]=Berlin&address[country]=DE
# Arrays with indices
items[0]=first&items[1]=second
# Complex nested structures (e.g., Stripe API style)
flow_data[subscription][items][0][id]=si_123&flow_data[subscription][items][0][quantity]=2
Type conversion is handled automatically:
| Form Value | Converted To |
|---|---|
true, false |
bool |
42, -10 |
int64 |
3.14, 0.05 |
float64 |
| Other values | string |
Conservative conversion preserves string values that look like numbers but shouldn't be converted:
- Phone numbers starting with
+(e.g.,+1234567890) - Values with leading zeros (e.g.,
00123) - Values containing spaces or parentheses
This enables seamless integration with APIs like Stripe that use complex form-encoded request bodies.
Response Data¶
Return a *<Operation>ResponseData from your service method:
func (s *Service) GetUser(ctx context.Context, opts *GetUserServiceRequestOptions) (*GetUserResponseData, error) {
user, err := s.db.FindUser(opts.PathParams.Id)
if err != nil {
return nil, err
}
if user == nil {
return NewGetUserResponseData(&GetUserResponse404{
Code: "not_found",
Message: "User not found",
}), nil
}
return NewGetUserResponseData(&GetUserResponse200{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}), nil
}
You can also set custom headers and status codes:
resp := NewGetUserResponseData(&GetUserResponse200{...})
resp.Status = 200
resp.Headers = http.Header{
"X-Custom-Header": []string{"value"},
}
return resp, nil
Integrating with Existing Applications¶
Adding to an Existing Router¶
Each framework has a NewRouter function to register routes:
import (
"github.com/go-chi/chi/v5"
handler "your-module/api"
)
func main() {
r := chi.NewRouter()
// Your existing routes
r.Get("/existing", existingHandler)
// Register generated API routes
svc := handler.NewService()
handler.NewRouter(r, svc)
http.ListenAndServe(":8080", r)
}
import (
"github.com/labstack/echo/v4"
handler "your-module/api"
)
func main() {
e := echo.New()
// Your existing routes
e.GET("/existing", existingHandler)
// Register generated API routes
svc := handler.NewService()
handler.NewRouter(e, svc)
e.Start(":8080")
}
import (
"github.com/gin-gonic/gin"
handler "your-module/api"
)
func main() {
r := gin.Default()
// Your existing routes
r.GET("/existing", existingHandler)
// Register generated API routes
svc := handler.NewService()
handler.NewRouter(r, svc)
r.Run(":8080")
}
Adding Middleware¶
Use WithMiddleware to add framework-specific middleware:
svc := handler.NewService()
handler.NewRouter(r, svc,
handler.WithMiddleware(handler.ExampleMiddleware()),
handler.WithMiddleware(loggingMiddleware),
)
Testing¶
The generated code is designed for easy testing. Use the Handler() function (available for frameworks with custom signatures) or create a test server:
func TestGetUser(t *testing.T) {
// Create a mock service
svc := &MockService{
GetUserFunc: func(ctx context.Context, opts *api.GetUserServiceRequestOptions) (*api.GetUserResponseData, error) {
return api.NewGetUserResponseData(&api.GetUserResponse200{
Id: opts.PathParams.Id,
Name: "Test User",
Email: "test@example.com",
}), nil
},
}
// Create handler
handler := api.Handler(svc) // or api.NewRouter(chi.NewRouter(), svc)
// Test with httptest
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, 200, rec.Code)
}
Error Handling¶
The generated code includes a flexible error handling system that separates error classification from error response formatting.
Error Types¶
The HTTPAdapter handles four types of errors:
| Error Kind | Description | Default Status |
|---|---|---|
OapiErrorKindParse |
Parameter parsing errors (invalid path/query/header) | 400 |
OapiErrorKindDecode |
Request body decoding errors (invalid JSON, form data) | 400 |
OapiErrorKindValidation |
Request validation errors (failed schema validation) | 400 |
OapiErrorKindService |
Service/business logic errors from your implementation | 500 (or typed) |
Default Behavior¶
The OapiDefaultErrorHandler respects the Accept header:
- JSON (
application/json,*/*, or empty): Returns JSON response - Other: Returns plain text
// JSON error response for parse/decode/validation errors
{
"error": "invalid parameter \"id\": strconv.Atoi: parsing \"abc\": invalid syntax"
}
Service errors are JSON-encoded directly, so the response structure matches your error type's JSON tags.
Custom Error Handler¶
Implement the OapiErrorHandler interface to customize error responses, add logging, or collect metrics:
type OapiErrorHandler interface {
HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error)
}
The err parameter is either:
OapiHandlerError- A generic handler error (parse, decode, validation) with context fields- Typed error - An error type from your OpenAPI spec (when
error-mappingis configured)
type OapiHandlerError struct {
Kind OapiErrorKind // Type of error (Parse, Decode, Validation, Service)
OperationID string // OpenAPI operation ID (e.g., "GetUser", "CreateOrder")
Message string // Error message
ParamName string // Parameter name (for parse errors)
ParamLocation string // Parameter location: "path", "query", "header" (for parse errors)
Err error // Underlying error, exposed via Unwrap()
}
OapiHandlerError implements Unwrap(), so errors.Is and errors.As reach the
error that caused it. With generate.handler.validation.request: true, request
validation failures wrap a runtime.ValidationErrors, which lets a custom handler
return field-level details instead of parsing Message:
func (h *ValidationErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) {
var validationErrs runtime.ValidationErrors
if errors.As(err, &validationErrs) {
fields := make(map[string]string, len(validationErrs))
for _, ve := range validationErrs {
fields[ve.Field] = ve.Message
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(map[string]any{"fields": fields})
return
}
// Fall back to the default behavior for everything else
(&api.OapiDefaultErrorHandler{}).HandleError(w, r, statusCode, err)
}
Parse and decode errors wrap their cause the same way, so errors.As also reaches
types like *strconv.NumError or *json.SyntaxError.
Example custom handler with logging:
type LoggingErrorHandler struct {
logger *slog.Logger
}
func (h *LoggingErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, statusCode int, err error) {
// Check if it's a generic handler error
if handlerErr, ok := err.(api.OapiHandlerError); ok {
h.logger.Error("request error",
"operation", handlerErr.OperationID,
"kind", handlerErr.Kind,
"error", handlerErr.Message,
"status", statusCode,
"param", handlerErr.ParamName,
"param_location", handlerErr.ParamLocation,
)
} else {
// Typed error from OpenAPI spec or service error
h.logger.Error("service error",
"error", err.Error(),
"status", statusCode,
)
}
// Write response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if handlerErr, ok := err.(api.OapiHandlerError); ok {
json.NewEncoder(w).Encode(map[string]string{
"error": handlerErr.Message,
})
} else {
// Typed error - encode directly to match API contract
json.NewEncoder(w).Encode(err)
}
}
Use your custom handler with WithErrorHandler:
svc := api.NewService()
handler := api.NewRouter(svc,
api.WithErrorHandler(&LoggingErrorHandler{logger: slog.Default()}),
)
Typed Error Responses¶
When your OpenAPI spec defines error response types and you configure error-mapping, the generator creates typed errors with constructors:
# cfg.yaml
error-mapping:
InvalidRequestError: error.message
This generates:
func NewInvalidRequestError(message string) InvalidRequestError {
return InvalidRequestError{ErrorData: &ErrorData{Message: message}}
}
Use in your service implementation:
func (s *Service) CreateUser(ctx context.Context, opts *CreateUserOpts) (*CreateUserResponse, error) {
if opts.Body.Email == "" {
return nil, NewInvalidRequestError("email is required")
}
// ...
}
The HTTPAdapter automatically detects typed errors and uses the appropriate status code from your OpenAPI spec.
Union (oneOf/anyOf) error types get an Error() method but no constructor, since a message alone doesn't say which variant to build. For operations that return them, the adapter reports parse, decode, and validation failures as OapiHandlerError.
Examples¶
Complete examples for each framework are available in the repository:
Each example includes:
cfg.yml- Configuration fileapi/gen.go- Generated handler codeapi/service.go- Service implementation scaffoldapi/middleware.go- Middleware scaffoldserver/main.go- Runnable serverREADME.md- Framework-specific documentation