Skip to content

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:

  1. Service Interface - A Go interface defining your business logic methods
  2. HTTP Adapter - Generated code that handles HTTP parsing and calls your service
  3. Router Registration - Framework-specific code to register routes
  4. Scaffold Files - One-time generated files for your implementation (service.go, middleware.go)
  5. Server Main (optional) - A runnable main.go with 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-mapping is 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 file
  • api/gen.go - Generated handler code
  • api/service.go - Service implementation scaffold
  • api/middleware.go - Middleware scaffold
  • server/main.go - Runnable server
  • README.md - Framework-specific documentation