API Standards
This page is the in-module API cheatsheet. The full design contract
lives in ../api-design.md — read that first.
Framework
Section titled “Framework”SHC uses go-chi with Unix socket communication between CLI and
server. Modules build their own http.Handler and the daemon mounts
them at boot via the registry pattern.
Route structure
Section titled “Route structure”// modules/{module}/router.gopackage {module}
import ( "net/http"
"github.com/go-chi/chi/v5"
"gitlab.com/bitspur/selfhosted-cloud/ce/core/api" "gitlab.com/bitspur/selfhosted-cloud/ce/core/registry" "gitlab.com/bitspur/selfhosted-cloud/ce/core/validation")
func buildRouter(_ registry.Deps) http.Handler { r := chi.NewRouter() r.Post("/api/method/shc.{module}.create", createHandler()) return r}Method naming
Section titled “Method naming”Use JSON-RPC style method names:
| Operation | Pattern | Example |
|---|---|---|
| List | shc.{module}.list | shc.backup.list |
| Get | shc.{module}.get | shc.backup.get |
| Create | shc.{module}.create | shc.backup.create |
| Update | shc.{module}.update | shc.backup.update |
| Delete | shc.{module}.delete | shc.backup.delete |
Request / response types
Section titled “Request / response types”Plain Go structs with json: tags for the wire shape and
validate: tags for input rules.
type BackupCreate struct { StackName string `json:"stack_name" validate:"required,min=1,max=255"` Environment string `json:"environment,omitempty"` IncludeVault bool `json:"include_vault,omitempty"`}
type BackupResponse struct { ID string `json:"id"` StackName string `json:"stack_name"` CreatedAt time.Time `json:"created_at"` SizeBytes int64 `json:"size_bytes"`}Wire-level envelopes ({"message": ...} and {"data": ...}) come
from core/api:
api.WriteJSON(w, api.MethodResponse[BackupResponse]{Message: out})Context injection
Section titled “Context injection”The chi middleware stack populates a *ctx.Ctx from request headers.
Handlers read it via ctx.FromRequest(r).
func createHandler(svc *Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := ctx.FromRequest(r) var req BackupCreate if err := validation.DecodeAndValidate(r.Body, &req); err != nil { api.WriteError(w, err) return } out, err := svc.Create(r.Context(), c, req) if err != nil { api.WriteError(w, err) return } api.WriteJSON(w, api.MethodResponse[BackupResponse]{Message: out}) }}Error handling
Section titled “Error handling”Coded errors flow through api.WriteError, which type-asserts to
*coded.CodedError and emits the canonical
{error_code, message, http_status} envelope.
func getStackHandler(svc *Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req GetRequest if err := validation.DecodeAndValidate(r.Body, &req); err != nil { api.WriteError(w, err) return } stack, err := svc.Get(r.Context(), req.Name) if err != nil { api.WriteError(w, err) return } if stack == nil { api.WriteError(w, X404908STKNFD(req.Name)) return } api.WriteJSON(w, api.MethodResponse[*StackResponse]{Message: stack}) }}Pagination
Section titled “Pagination”Every GET /api/resource/<Collection> listing follows one contract
(parse the params with api.ParseListPage, page materialised slices
with api.PageSlice — see core/api/pagination.go):
- Query params:
limit(page size;0= everything, overriding any default) andoffset(rows to skip). Entity collections (App, Stack, Node, Tenant, Repo, Usage, Secret, Backup, Worker) default tolimit=0; append-only logs default to a server page and may cap explicit limits (Audit 100/1000, JobRun 50/100, Task 50/500). - Response envelope: always
total_count(rows matching the filter, ignoring paging) andhas_more(rows exist beyond this page) — an applied limit is never silent. Cursor-capable logs (Audit, JobRun) additionally acceptcursorand returnnext_cursor;offsetis ignored while a cursor is in play. - Every listing has a deterministic total order (ORDER BY with an id tie-break, or an in-handler sort) so pages never shuffle.
type ListResponse struct { Data []Item `json:"data"` TotalCount int `json:"total_count"` HasMore bool `json:"has_more"` NextCursor *string `json:"next_cursor,omitempty"` // cursor-capable logs only}Headers
Section titled “Headers”Context is passed via headers:
| Header | Purpose |
|---|---|
X-SHC-Tenant | Tenant name |
X-SHC-Stack | Stack name |
X-SHC-Environment | Environment name |
X-SHC-Username | Unix username (from CLI) |
X-Forwarded-For | Client IP (from proxies) |