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-alias¶
When models are in a separate package, prefix types with this alias.
generate:
models: false # Don't generate models here
handler:
kind: chi
models-package-alias: types # Use types.User instead of User
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)
}
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.
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