CLI Reference

add handler

Add an endpoint, TypeSpec operation plus handler method skeleton.
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. 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).
  2. Adds the handler method to a category file under internal/modules/<module>/surfaces/<surface>/http/: <module>_handler.go for the base category, <module>_<group>_handler.go otherwise. The group defaults to the --path's first non-parameter segment (/notifications/{id}/read -> notifications); override it with --group. The category's first operation creates the file; later ones are inserted as a new method at the file's trailing // gpsystem:handlers anchor. The stub returns 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: it carries no handler methods.

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