CLI Reference

add handler

Add an endpoint, TypeSpec operation plus handler method skeleton.

add handler adds a single endpoint to an existing surface: a TypeSpec operation plus a handler method stub, wired into the surface's category files and anchors. Reach for it every time you add an operation to a surface that add surface already scaffolded.

go tool gpsystem add handler <module> <surface> <operation> \
  --method GET --path "/items/{id}"
FlagDefaultMeaning
--methodGETone of GET, POST, PUT, PATCH, DELETE
--path/route path relative to the surface base; {name} segments become path parameters
--permissionnoneRBAC permission for the operation; emits a @permission("...") decorator into the .tsp
--policynoneper-user policy name; emits a @policy("...") decorator into the .tsp; register the name in the surface's policy/policy.go
--groupnone (the --path's first literal segment)the handler-file category, e.g. notifications; an empty group lands in the module's base handler file

What it does

  1. Inserts a TypeSpec operation skeleton at the gpsystem:operations anchor in <surface>.tsp: path parameters declared, a @body parameter added for POST/PUT/PATCH, return type stubbed.
  2. With --permission / --policy the matching decorators are added and the response union widens with Unauthorized | Forbidden; enforcement happens in the generated middleware (see auth, rbac & policy). Once add auth has run, this works out of the box: the auto-wired identity middleware puts the caller's identity in the context, and the enforcer decides. Without add auth, a tagged operation fails closed: with no identity in the context, it returns 401 until authentication is wired.
  3. Picks the category file the handler method lands in, under internal/modules/<module>/surfaces/<surface>/http/: <module>_handler.go for the base category, <module>_<group>_handler.go otherwise.
  4. Derives the group from the --path's first non-parameter segment by default (/notifications/{id}/read -> notifications); override it with --group.
  5. Creates the category file on its first operation; later operations in the same category are inserted as a new method at the file's trailing // gpsystem:handlers anchor.
  6. Stubs the method body to return 501 Not Implemented as a problem document, so the endpoint is immediately visible and testable.

handler.go stays the Handler struct, New and //go:generate home throughout: it carries no handler methods of its own.

The workflow

go tool gpsystem add handler news api getNews --method GET --path "/items/{id}"
mise run generate     # materializes gen.GetNewsRequest / gen.GetNewsResponse

getNews groups under items because of its /items/{id} path: internal/modules/news/surfaces/api/http/news_items_handler.go. A later operation in the same group (e.g. deleteNews --path "/items/{id}") lands in the same file, next to the existing method.

Then replace the stub body: call the service and wrap its error at the boundary, the same pattern the add surface skeleton generates:

// internal/modules/news/surfaces/api/http/news_items_handler.go
func (h *Handler) GetNews(ctx context.Context, req gen.GetNewsRequest) (gen.GetNewsResponse, error) {
    item, err := h.svc.GetByID(ctx, req.Id)   // add the service method
    if err != nil {
        return nil, errs.Wrap(err, "api: get news")  // carries a stack; httperr renders it
    }
    return mapper.ToGetNewsResponse(item), nil // add the mapper
}
Between add handler and mise run generate the project does not compile: the handler references generated types that do not exist yet. That is expected; the generate step closes the gap.

Refine the operation in the .tsp file (models, error unions, pagination) and re-run mise run generate: the strict interface keeps your handler honest at compile time.

Copyright © 2026