Skip to content

Handlers & Resources

ShipQ’s API layer is intentionally simple: handlers register themselves with metadata, then the handler compiler generates everything around them — server wiring, OpenAPI specs, test clients, and TypeScript clients.

The fastest way to get a working API is shipq resource, which generates querydefs, handlers, and tests in one shot.

Terminal window
shipq resource pets all

This creates:

FileMethodRouteDescription
api/pets/create.goPOST/petsCreate a pet
api/pets/get_one.goGET/pets/:idGet a single pet by ID
api/pets/list.goGET/petsList pets (with pagination)
api/pets/update.goPATCH/pets/:idUpdate a pet
api/pets/soft_delete.goDELETE/pets/:idSoft-delete a pet
api/pets/register.goHandler registration function

Plus query definitions in querydefs/ and test files in api/pets/spec/.

You don’t have to generate all five at once:

Terminal window
shipq resource pets create
shipq resource pets get_one
shipq resource pets list
shipq resource pets update
shipq resource pets delete

If you’ve run shipq auth, routes are auth-protected by default (controlled by protect_by_default = true in shipq.ini). To make routes public:

Terminal window
shipq resource pets all --public

Every handler in ShipQ registers itself with metadata that the handler compiler uses for code generation. The register.go file in each API package is responsible for this.

When you write (or generate) a handler, it describes itself via a handler.HandlerInfo struct:

  • HTTP routing: method (GET, POST, PATCH, DELETE) and path (e.g., /pets/:id)
  • Path parameters: automatically extracted from the path pattern (e.g., id from /pets/:id)
  • Authentication: whether the handler requires auth, allows optional auth, or is fully public
  • Request type: the struct that represents the request body (nil for GETs with no body)
  • Response type: the struct that represents the response body

This metadata is what powers OpenAPI generation, TypeScript client codegen, test client generation, and admin UI generation.

Handlers use plain Go structs for request and response types. ShipQ inspects struct tags to determine JSON field names, required fields, and omitempty behavior:

type CreatePetRequest struct {
Name string `json:"name"`
Species string `json:"species"`
Age int `json:"age"`
}
type CreatePetResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Species string `json:"species"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
}

Fields without omitempty and that aren’t pointers are treated as required in the OpenAPI spec.

After you’ve added or modified handlers, compile the handler registry:

Terminal window
shipq handler compile

This runs the handler compiler, which:

  1. Discovers all handler registration functions across your api/ packages
  2. Extracts full metadata: HTTP method, path, path params, request/response struct definitions, auth requirements
  3. Generates all the downstream artifacts
ArtifactLocationDescription
Server maincmd/server/main.goRunnable HTTP server with all handlers wired up
OpenAPI specGET /openapi (dev/test)OpenAPI 3.1 JSON spec
Docs UIGET /docs (dev/test)Interactive API documentation
Admin UIGenerated from OpenAPIUseful for manual testing
HTTP test clientapi/*/spec/Typed test client for each resource
Test harnessapi/*/spec/Test setup/teardown helpers
Integration testsapi/*/spec/RBAC, tenancy, and 401 tests
TypeScript clientConfigurable output dirTyped HTTP client for your frontend
React/Svelte hooksConfigurable output dirOptional framework-specific helpers

For more control over what gets generated, you can use handler generate instead of resource:

Terminal window
shipq handler generate posts

This generates the same CRUD handler files but gives you more flexibility to customize before compiling.

Generated handler files are yours to modify — they’re not overwritten on subsequent compiles unless they have the // Code generated by shipq. DO NOT EDIT. header or a zz_generated_ filename prefix.

  • api/<table>/create.go
  • api/<table>/get_one.go
  • api/<table>/list.go
  • api/<table>/update.go
  • api/<table>/soft_delete.go

These are overwritten on every shipq handler compile:

  • Any file starting with zz_generated_
  • Any file with the // Code generated by shipq. DO NOT EDIT. header
  • cmd/server/main.go

You can write handlers from scratch alongside generated ones. The pattern is:

  1. Create an API package (e.g., api/reports/)
  2. Write your handler function with request/response structs
  3. Write a register.go that registers the handler with the registry
  4. Add query definitions in querydefs/ if you need custom queries
  5. Run shipq handler compile to regenerate server wiring

As long as your handler registers itself correctly, ShipQ’s handler compiler will pick it up and include it in the OpenAPI spec, test client, TypeScript client, and server wiring.

ShipQ handles foreign key relationships naturally. If you create a books table that references authors:

Terminal window
shipq migrate new authors name:string bio:text
shipq migrate new books title:string author_id:references:authors
shipq resource authors all
shipq resource books all

The generated books handlers will include the author_id field in request/response types, and the OpenAPI spec will reflect the relationship. Foreign key columns are resolved to their correct types (e.g., the author_id field in the request body will reference the appropriate type, not a raw int64).

The typical handler development loop:

Terminal window
# 1. Generate or edit handlers
shipq resource <table> all
# ... or hand-edit api/<table>/*.go
# 2. Add or edit query definitions
# ... edit querydefs/<table>/queries.go
# 3. Recompile queries (if querydefs changed)
shipq db compile
# 4. Recompile handler registry (always needed after handler changes)
shipq handler compile
# 5. Run tests
go test ./... -v
# 6. Start the server
go run ./cmd/server