Help us shape the future of Go! Take the 2026 Go Developer Survey

apiggo

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

apiggo

apiggo logo

Go Reference Go Version License

Contract-first OpenAPI → Go code generator with a thin net/http runtime.

Point it at an OpenAPI document and it emits typed DTOs, HTTP adapters, and handler stubs — so the only code you write is business logic inside Handle(ctx, in) (out, error).

Features

  • Contract-first. The OpenAPI document is the single source of truth — the spec and the implementation can't silently drift.
  • Typed end to end. Path/query/header params, JSON bodies, responses, enums, and per-operation error responses all become concrete Go types.
  • You write only the interesting part. Parameter parsing, type conversion, JSON (de)serialization, and error mapping are generated — your job is the body of Handle.
  • Safe regeneration. DTOs and HTTP adapters are regenerated on every run; handler stubs are written once and never overwritten, so re-running after a spec change never clobbers your logic.
  • Zero framework. Generated and runtime code use only stdlib net/http (Go 1.22+ method-aware ServeMux) — no router dependency, no reflection magic.
  • Batteries-included runtime. Graceful shutdown, panic recovery, a middleware chain, and unified error handling.

Overview

apiggo treats your OpenAPI spec as the single source of truth. From one document it generates three cooperating layers of Go code and ships a small runtime that hosts them:

  • DTOs — request/response structs, enums, and typed errors derived from the schema.
  • HTTP adapters — handlers that parse parameters and bodies, call your code, and serialize the result.
  • Handler stubs — minimal Handle(ctx, in) (out, error) scaffolds where your logic lives.

Everything the generator and the runtime produce depends only on the standard library net/http (using Go 1.22+ method-aware ServeMux patterns) plus the tiny apiggo/server package.

How it works

Every operation in the spec flows through three layers:

OpenAPI spec ──apiggo──▶  DTOs         (types you pass around)
                           HTTP adapter (transport: parse → call → serialize)
                           Handler stub (your business logic)

A generated run writes a tree like this:

.
├── generated/
│   ├── dto/dto.go          # types, enums, typed errors  (regenerated)
│   └── router/router.go    # one registrar per operation (regenerated)
└── api/
    ├── getpet/handler.go   # your logic — written once, never overwritten
    └── createpet/handler.go

The core idea: the generator owns transport, you own logic. The HTTP adapter knows how to decode a request into a typed In, hand it to your Handle, and turn the returned Out (or error) into an HTTP response. It calls into the runtime for wiring — the adapter itself knows nothing about middleware, panic recovery, or how errors become status codes. That keeps generated code small and your code free of transport concerns.

Errors are just values. A handler returns any error; if it satisfies the runtime's APIError interface (StatusCode() int + Body() any), the runtime maps it to that status and JSON body. The typed error structs generated from your spec's error responses implement exactly that interface, so returning &dto.GetPetNotFound{...} yields a real 404. Anything else falls back to 500.

Install

go install github.com/siyoga/apiggo/cmd/apiggo@latest

Quick start

1. Describe your API
# api/openapi.yaml
openapi: 3.0.3
info: { title: petstore, version: 1.0.0 }
paths:
  /pets:
    post:
      operationId: createPet
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PetCreate' }
      responses:
        '200':
          description: created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pet' }
components:
  schemas:
    Pet:
      type: object
      required: [id, name]
      properties:
        id:   { type: integer, format: int64 }
        name: { type: string }
    PetCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
2. Generate
apiggo -schema api/openapi.yaml -module github.com/acme/svc

This writes generated/dto, generated/router, and an api/<operation> stub per operation.

3. Implement the handler

Fill in the generated stub — this file is yours from now on:

// api/createpet/handler.go
package createpet

import (
	"context"

	dto "github.com/acme/svc/generated/dto"
)

type Handler struct{}

func New() *Handler { return &Handler{} }

func (h *Handler) Handle(ctx context.Context, in dto.CreatePetIn) (dto.CreatePetOut, error) {
	pet := dto.Pet{Id: 1, Name: in.Body.Name}
	return pet, nil
}
4. Wire it up and serve

WithOpenAPIMethod pairs a generated registrar with your handler; Serve blocks until the context is cancelled, then shuts down gracefully.

package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/siyoga/apiggo/server"

	"github.com/acme/svc/api/createpet"
	"github.com/acme/svc/generated/router"
)

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	h := createpet.New()

	srv := server.NewServer(
		server.WithShutdownTimeout(10*time.Second),
		server.WithOpenAPIMethod(router.CreatePet, h.Handle),
	)

	if err := srv.Serve(ctx, ":8080"); err != nil {
		log.Fatal(err)
	}
}

Generated code at a glance

You never edit these, but it helps to see the shape. DTOs — typed structs, enums, and errors:

type CreatePetIn struct {
	Body PetCreate `json:"-"`
}

type CreatePetOut = Pet

type GetPetNotFound struct {
	Message *string `json:"message,omitempty"`
}

func (e *GetPetNotFound) Error() string   { return "Not Found" }
func (e *GetPetNotFound) StatusCode() int { return 404 }
func (e *GetPetNotFound) Body() any       { return e }

HTTP adapters — one registrar per operation that parses the request, calls your handler, and serializes the response:

// GetPet registers the GET /pets/{id} operation.
func GetPet(h func(ctx context.Context, in dto.GetPetIn) (dto.GetPetOut, error)) func(*server.Server) {
	return func(s *server.Server) {
		s.Register(http.MethodGet, "/pets/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var in dto.GetPetIn
			{
				raw := r.PathValue("id")
				v, err := strconv.ParseInt(raw, 10, 64)
				if err != nil {
					s.HandleError(w, r, server.ErrBadRequest("invalid parameter: id"))
					return
				}
				in.Id = v
			}
			in.XTrace = r.Header.Get("X-Trace")
			out, err := h(r.Context(), in)
			if err != nil {
				s.HandleError(w, r, err)
				return
			}
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(out)
		}))
	}
}

Graceful shutdown

The runtime is built around clean shutdown. Serve blocks until either the server fails to start or the context is cancelled — and cancellation is the caller's responsibility, typically a signal-bound context:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

srv := server.NewServer(server.WithShutdownTimeout(10 * time.Second))
if err := srv.Serve(ctx, ":8080"); err != nil {
	log.Fatal(err)
}

Under the hood Serve wires two mechanisms off the same ctx:

  1. http.Server.BaseContext is set to ctx. Every request context — and the ctx your handler receives — descends from it, so cancelling ctx immediately signals in-flight handlers to wind down.
  2. http.Server.Shutdown runs with a budget. On cancellation, Serve stops accepting connections and drains outstanding requests within WithShutdownTimeout (unbounded if unset).

Handlers that observe ctx exit fast; slow ones are allowed to finish up to the shutdown budget.

Serve(ctx, addr)
  ├─ startup failure (e.g. address in use)  ──▶ returned immediately
  └─ ctx cancelled
        ├─ BaseContext(ctx) cancels all in-flight request contexts
        └─ Shutdown(shutdownTimeout) drains connections
              ├─ drained in time  ──▶ returns nil
              └─ budget expired    ──▶ returns context.DeadlineExceeded

http.ErrServerClosed is the normal result of a graceful shutdown and is treated as success. Read/write/idle timeouts are configured the same way, via WithReadTimeout, WithWriteTimeout, and WithIdleTimeout (unset means "no timeout", standard net/http semantics).

Go cannot forcibly kill a goroutine — "cancelling" a handler means its context.Context is cancelled and the handler must observe it and return. A handler that ignores ctx runs until it finishes or the shutdown budget expires.

Status

apiggo is early-stage and evolving. Current scope:

  • OpenAPI 3.0.x specs (YAML or JSON).
  • application/json request and response bodies.
  • Scalar path, query, and header parameters with automatic type conversion.
  • Enums, nested objects, arrays, and per-operation typed error responses.
  • Standard-library net/http output (no external router).

Every operation must declare an operationId — it's used to derive stable Go identifiers and package names.

Contributing

Issues and pull requests are welcome. Run the test suite with go test ./..., and see CONTRIBUTING.md for the development loop and the golden-fixture workflow. Participation is governed by our Code of Conduct.

License

Licensed under the Apache License 2.0.

Documentation

Overview

Package apiggo turns an OpenAPI contract into Go source: it loads the spec, builds an intermediate representation, and renders three layers — DTOs, HTTP adapters, and handler stubs. The generated HTTP layer depends only on the standard library plus the companion runtime in github.com/siyoga/apiggo/server.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Generate

func Generate(cfg Config) error

Generate runs one generation pass: load contract -> build IR -> render layers.

Types

type Config

type Config struct {
	// ContractPath is the path to the OpenAPI contract (YAML or JSON).
	ContractPath string
	// OutRoot is the directory the generated tree is written under.
	OutRoot string
	// Module is the Go module path of the target project, used to build import
	// paths in the generated code (e.g. "github.com/acme/svc").
	Module string

	// DTOPkg, RouterPkg and APIPkg are module-relative package paths for the
	// three layers.
	DTOPkg    string // default "generated/dto"
	RouterPkg string // default "generated/router"
	APIPkg    string // default "api"
}

Config drives a single generation run.

type EnumDef

type EnumDef struct {
	Base   string // "string" or "int64"
	Values []EnumValue
}

EnumDef is the enum payload of a NamedType.

type EnumValue

type EnumValue struct {
	Name    string
	Literal string // rendered literal: `"red"` or `1`
}

EnumValue is one enum constant.

type Field

type Field struct {
	GoName string
	Type   string // Go type expression, in dto-package terms
	JSON   string // full struct tag body, e.g. `json:"name,omitempty"`
}

Field is one struct field.

type NamedType

type NamedType struct {
	Name string
	Doc  string

	// exactly one of the following is populated
	Struct []Field
	Enum   *EnumDef
	Alias  string // for `type Name = Alias`

	// APIError marks a generated error type; when set the template emits the
	// APIError methods (StatusCode/Body/Error).
	APIError    bool
	Status      int
	MessageExpr string // expression returned by Error(), e.g. e.Message or a literal
}

NamedType is a top-level generated type in the dto package.

type Operation

type Operation struct {
	ID      string // PascalCase base (from operationId)
	Method  string // "GET"
	Pattern string // "/users/{id}" (Go 1.22 mux syntax matches OpenAPI)
	Params  []Param
	HasBody bool
	InType  string // "<ID>In"
	OutType string // "<ID>Out"
	Errors  []string
}

Operation is one contract operation to emit a descriptor + stub for.

type Param

type Param struct {
	Wire     string // wire name ("id")
	GoName   string // field name ("Id")
	In       string // "path" | "query" | "header"
	Prim     string // "string"|"int64"|"int32"|"float64"|"float32"|"bool"
	Required bool
}

Param is a path/query/header parameter carried into the In struct.

type Spec

type Spec struct {
	Named      []NamedType // all dto types, sorted by name
	Operations []Operation
}

Spec is the generator-facing view of a whole contract.

Directories

Path Synopsis
cmd
apiggo command
Command apiggo generates dto, http-adapter and handler-stub code from an OpenAPI contract.
Command apiggo generates dto, http-adapter and handler-stub code from an OpenAPI contract.
examples
petstore command
Command petstore is the apiggo end-to-end example: an OpenAPI contract (api/openapi.yaml) generated into DTOs + HTTP adapters (generated/) with handler stubs (api/) implemented against an in-memory store, all wired onto the apiggo runtime and served over HTTP.
Command petstore is the apiggo end-to-end example: an OpenAPI contract (api/openapi.yaml) generated into DTOs + HTTP adapters (generated/) with handler stubs (api/) implemented against an in-memory store, all wired onto the apiggo runtime and served over HTTP.
petstore/api/createpet
Package createpet implements the CreatePet operation handler for the petstore example.
Package createpet implements the CreatePet operation handler for the petstore example.
petstore/api/getpet
Package getpet implements the GetPet operation handler for the petstore example.
Package getpet implements the GetPet operation handler for the petstore example.
petstore/generated/dto
Code generated by apiggo.
Code generated by apiggo.
petstore/generated/router
Code generated by apiggo.
Code generated by apiggo.
petstore/store
Package store is a tiny in-memory pet repository.
Package store is a tiny in-memory pet repository.
Package server is the thin net/http runtime that hosts apiggo-generated code: option-based configuration, a middleware chain, panic recovery, unified error handling via the APIError interface, and graceful shutdown.
Package server is the thin net/http runtime that hosts apiggo-generated code: option-based configuration, a middleware chain, panic recovery, unified error handling via the APIError interface, and graceful shutdown.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL