- Docs
- HTTP APIs
- Requests and responses
Requests and responses
Decode JSON bodies, read params and headers, paginate lists, and write consistent JSON responses.
Handlers stay close to net/http: decode the request, run your logic, encode a JSON envelope. No magic binding or config block.
Handle a request
Section titled “Handle a request”Container is for services, not I/O:
c := container.New(config.Default())db, _ := c.DB()r, _ := c.Router()r.Handle("POST", "/v1/users", createUser(db))func createUser(db db.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var in struct { Email string `json:"email"` Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&in); err != nil { writeError(w, http.StatusBadRequest, "invalid body") return } if strings.TrimSpace(in.Email) == "" { writeError(w, http.StatusUnprocessableEntity, "email: required") return } writeJSON(w, http.StatusCreated, map[string]string{"email": in.Email}) }}
func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v)}
func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg})}curl -s -X POST localhost:8080/v1/users -H 'Content-Type: application/json' -d '{"email":"a@example.com","name":"Ada"}'{"email":"a@example.com"}Success returns the resource (200/201); errors use {"error": "<stable message>"}. Set Content-Type before WriteHeader.
Read the request
Section titled “Read the request”| Source | How |
|---|---|
| Path params | router.Param(req, name), router.ParamNames(req) |
| Query | r.URL.Query().Get(key) |
| Headers | r.Header.Get(key) |
| JSON body | json.NewDecoder(r.Body).Decode(&v) |
| Body cap | http.MaxBytesReader(w, r.Body, n) |
| Test params | router.WithParams(ctx, params) |
| Cancellation | r.Context(), into DB, cache, queue calls |
Cap public bodies (413) and pass r.Context() downstream for middleware.Timeout.
Paginate lists
Section titled “Paginate lists”.zen RPCs flagged paginated: true generate cursor scaffolding; expose cursors, not offsets:
GET /v1/orders?cursor=eyJpZCI6NDJ9&limit=50The synthesized request message carries cursor and limit fields (so gRPC clients paginate too, and a paginated RPC may not declare params of those names). The generated service method takes the opaque cursor string plus limit int32. Both transports apply the same bounds before the service runs: zero becomes the default (20), a limit above the max (100) is clamped, and a negative limit is InvalidArgument. The response is the protobuf <Op>Response message (items plus next_cursor, empty when last).
key := orm.NewCursorKey(UserID, lastSeenID)users, err := orm.NextPage( orm.From(Users).OrderBy(UserID.Asc()), key,).Limit(50).All(ctx, conn)orm.OffsetPage(q, page, size) applies a 1-based LIMIT/OFFSET and returns (Query, error). For keyset paging, CursorKey.Encode() and orm.DecodeCursor(token, col) round-trip the cursor, and orm.AfterTuple builds multi-column predicates.
Troubleshooting
Section titled “Troubleshooting”An empty body decodes without error
r.Body was consumed or nil. Use httptest.NewRequest with a strings.Reader.
Params are always empty under test
Plain test requests carry no driver context. Seed with router.WithParams.
The client sees Content-Type: text/plain
WriteHeader ran before the header. Set Content-Type, then status, then body.
Huge uploads exhaust memory
No MaxBytesReader: wrap r.Body, return 413.
Deadline middleware has no effect
A handler or ORM call uses context.Background(). Thread r.Context() through.