rest

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 25, 2026 License: MIT Imports: 38 Imported by: 0

Documentation

Overview

Package rest's collector extracts an OpenAPI document from the registered HTTP handlers + their reflected I/O types.

Package rest provides the kit's HTTP/REST server, client, and route-registration primitives. It is opinionated about how services describe their HTTP surface and how that surface maps to the application layer:

  • Routes are declared by Controllers via Routes(Router) — a grouped-routing API on top of stdlib net/http.ServeMux that supports prefix groups (Router.Route), scoped middleware (Router.Use and Router.With), and per-method handler registration.
  • Handler factories take usecase-shaped inputs directly: usecase.Creator[R], usecase.Lister[R], usecase.Getter[R], and so on. There is no intermediate "controller" adapter; the kit translates HTTP query opts into search.Option internally.
  • The JSON:API wire format is the default. Plain-JSON variants live in handlers_convenience.go for the rare endpoint that genuinely needs one.

Route registration

A Controller implements one method:

func (c *workspaceController) Routes(r rest.Router) {
    r.Route("/v1/workspaces", func(r rest.Router) {
        r.Use(c.auth, c.actor)
        r.Post("/", rest.NewJsonApiCreateHandler(c.uc, api.WorkspaceFromDTO, api.WorkspaceToDTO))
        r.Route("/{id}", func(r rest.Router) {
            r.Use(c.tenancy)
            r.Get("/", rest.NewJsonApiGetHandler(c.uc, api.WorkspaceToDTO, nil))
            r.Delete("/", rest.NewJsonApiDeleteHandler(c.uc, repository.DeleteTypeSoft))
            r.Post("/members", rest.NewJsonApiCommandHandler(c.uc.Invite, decodeInvite, api.MemberToDTO))
        })
    })
}

Middleware ordering is first-in-list-is-outermost — r.Use(a, b, c) produces a(b(c(handler))). The same applies to global middleware registered via WithMiddlewares.

Handler factories

Resource CRUD (body equals the resource):

  • NewJsonApiCreateHandler — POST /resources
  • NewJsonApiGetHandler — GET /resources/{id} (or singleton via HandlerWithGetDecoderOpts(DecodeGetSkipURLPathID()))
  • NewJsonApiListHandler — GET /resources
  • NewJsonApiUpdateHandler — PUT /resources/{id}
  • NewJsonApiPatchHandler — PATCH /resources/{id}
  • NewJsonApiDeleteHandler — DELETE /resources/{id}

DDD command shape (usecase takes a typed Command, input split across body + path):

  • NewJsonApiCommandHandler — POST/PATCH /resources/{id}/action

Bulk:

  • NewJsonApiBulkCreateHandler — primary-data array
  • NewJsonApiBulkUpdateHandler — filter + patch envelope
  • NewJsonApiBulkDeleteHandler — filter envelope
  • NewJsonApiAtomicOperationsHandler — mixed add/update/remove in one transaction

Streaming:

  • NewMultipartUploadHandler — multipart/form-data with one file part
  • NewStreamingDownloadHandler — raw bytes with Range support
  • NewServerSentEventsHandler — text/event-stream

Escape hatch for the rare handler that doesn't fit any shape:

  • WriteJSONAPI — single-resource document with buffered marshal
  • WriteJSONAPIError — jsonapi error document via the kit encoder

Server bootstrap

server := rest.NewServer(
    rest.WithAddress(":8080"),
    rest.WithMiddlewares(
        rest.RequestIDMiddleware(),
        rest.RecoveryMiddleware(log),
        rest.AccessLogMiddleware(log),
    ),
    rest.WithControllers(workspaceCtrl, memberCtrl),
)
_ = server.ListenAndServe()

With fx, the wiring is:

fx.New(
    rest.FxModule(),
    rest.FxAuthenticator(),
    rest.NewFxController(NewWorkspaceController),
    rest.NewFxMiddleware(NewActorMiddleware),
    // ...
).Run()

Authentication

Auth is a regular Middleware. Wire it via rest.NewAuthMiddleware:

r.Use(rest.NewAuthMiddleware(authenticator))

Or scoped to a single registration:

r.With(rest.NewAuthMiddleware(authenticator)).Post("/", handler)

Client

rest.NewClient and rest.NewDefaultHTTPClient cover the service-to-service call side. NewTracingTransport wraps an http.RoundTripper with OpenTelemetry trace propagation.

Index

Constants

View Source
const HeaderRequestID = "X-Request-ID"

HeaderRequestID is the canonical request-id header read and written by the REST middleware stack.

View Source
const IDPath = "/{id}"

IDPath is the conventional URL segment for a resource id, used in Router registrations like r.Get(IDPath, h) when callers want the pattern to match the kit's "/{id}" convention exactly.

Variables

View Source
var ErrResponseWrongType = errors.New("response is not of the expected type")

Functions

func BuildHandler

func BuildHandler(controllers ...Controller) http.Handler

BuildHandler is a convenience for tests and embedded uses that want the same routing semantics as NewServer without the http.Server wrapper: it runs each Controller's Routes() against a fresh root Router, flattens to an *http.ServeMux, and returns it. Global middleware should be applied by the caller if needed.

func DELETE

func DELETE[DO, EI any](ctx context.Context, c Client, path string, req EI) (DO, error)

func DecodeEmptyReq

func DecodeEmptyReq(ctx context.Context, r *http.Request) (interface{}, error)

func DecodeGetReq

func DecodeGetReq(parseOpts []query.ParseOpt, opts ...GetDecoderOpt) func(_ context.Context, req *http.Request) ([]query.Option, error)

func DecodeTokenFromCtx

func DecodeTokenFromCtx(ctx context.Context, r *http.Request) (interface{}, error)

func DefaultErrorEncoder

func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter)

DefaultErrorEncoder encodes errors to HTTP responses. It does not log; an error-logging middleware in the chain is the right place to attribute errors to a request_id / span.

func EncoderAllowsEmptyRes

func EncoderAllowsEmptyRes(emptyResAllowed bool) encoderOpt

func FxAuthenticator

func FxAuthenticator() fx.Option

func FxModule

func FxModule(opts ...serverOption) fx.Option

func GET

func GET[DO, EI any](ctx context.Context, c Client, path string, req EI) (DO, error)

func GetJSONAPIIncludes

func GetJSONAPIIncludes(ctx context.Context) []string

GetJSONAPIIncludes extracts the includes from the context

func JSONErrorEncoder

func JSONErrorEncoder(_ context.Context, err error, w http.ResponseWriter)

JSONErrorEncoder encodes errors as simple JSON.

func JsonApiErrorEncoder

func JsonApiErrorEncoder(ctx context.Context, err error, w http.ResponseWriter)

func NewClient

func NewClient(
	clientName string,
	baseURL *url.URL,
	endpoints []ClientEndpoint,
	opts ...clientOption,
) (*client, error)

func NewCreateHandler

func NewCreateHandler[R resource.Resource, C usecase.Creator[R], O any](
	creator C,
	decoder func(O) R, encoder func(res R) O,
	opts ...HandlerOpt,
) http.Handler

func NewDefaultHTTPClient

func NewDefaultHTTPClient() *http.Client

NewDefaultHTTPClient returns an *http.Client with a tuned transport suitable for service-to-service calls. Use it as the http.Client passed to WithHTTPClient when the caller does not have a project-specific transport. The returned client uses Timeout=0 — pair with WithClientReqTimeout or per-request ctx deadlines.

func NewDeleteHandler

func NewDeleteHandler(
	deleter usecase.Deleter, deleteType repository.DeleteType,
	opts ...HandlerOpt,
) http.Handler

func NewFxController

func NewFxController(controller any) fx.Option

NewFxController registers a Controller constructor into the fx graph. The Controller is collected via the `restControllers` fx group and its Routes(Router) method is invoked at NewServer time against the server's root router.

func NewFxMiddleware

func NewFxMiddleware(middleware any) fx.Option

NewFxMiddleware is a helper function that given a middleware constructor builds a compatible and annotated fx module.

func NewGetHandler

func NewGetHandler[R resource.Resource, C usecase.Getter[R], O any](
	getter C, encoder func(res R) O,
	parseOpts []query.ParseOpt,
	opts ...HandlerOpt,
) http.Handler

func NewHTTPClientDecoder

func NewHTTPClientDecoder[O any](ctx context.Context, r *http.Response) (response O, err error)

func NewHTTPDecoder

func NewHTTPDecoder[O any](
	f func(context.Context, *http.Request) (request O, err error),
) func(context.Context, *http.Request) (request any, err error)

func NewHandler

func NewHandler[I, O any](
	e transport.Endpoint[I, O],
	reqDecoder DecodeRequestFunc,
	resEncoder EncodeResponseFunc,
	opts ...HandlerOpt,
) handler[I, O]

func NewJsonApiAtomicOperationsHandler

func NewJsonApiAtomicOperationsHandler(
	dispatcher func(ctx context.Context, ops []AtomicOperation) ([]AtomicOperationResult, error),
	opts ...HandlerOpt,
) http.Handler

NewJsonApiAtomicOperationsHandler wires the JSON:API Atomic Operations endpoint. The dispatcher receives the parsed operations array; it's the dispatcher's job to run them inside a single transaction and produce results in the same order.

This is the most flexible (and most dangerous) bulk shape — any mix of add/update/remove across any resource type in one request. Use only when true cross-resource atomicity is required.

func NewJsonApiBulkCreateHandler

func NewJsonApiBulkCreateHandler[R, DTO resource.Resource, C usecase.CreatorBatch[R]](
	creator C,
	decoder func(DTO) R, encoder func(res R) DTO,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiBulkCreateHandler wires `POST /resources` whose body is `{ "data": [ {...}, {...} ] }`. The usecase implements usecase.CreatorBatch[R] (one transactional batch insert); the response is a primary-data array with the created resources, status 201.

func NewJsonApiBulkDeleteHandler

func NewJsonApiBulkDeleteHandler[F any, Cmd any](
	cmdFn func(ctx context.Context, cmd Cmd) (int, error),
	cmdFromReq func(filter F) Cmd,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiBulkDeleteHandler wires `POST /resources:batchDelete`.

func NewJsonApiBulkUpdateHandler

func NewJsonApiBulkUpdateHandler[F any, P any, Cmd any](
	cmdFn func(ctx context.Context, cmd Cmd) (int, error),
	cmdFromReq func(filter F, patch P) Cmd,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiBulkUpdateHandler wires `POST /resources:batchUpdate`. The cmdFn signature is open enough to fit any (filter, patch) command shape; the kit provides the wire-envelope contract via BulkUpdateRequest. Returns 200 + a BulkUpdateResponse with the affected count.

type BulkMarkPaidCommand struct {
    Filter InvoiceFilter
    Patch  InvoicePatch
}
func (uc *invoiceUsecase) MarkPaid(ctx context.Context, cmd BulkMarkPaidCommand) (int, error)

r.Post("/invoices:batchUpdate",
    kitrest.NewJsonApiBulkUpdateHandler[InvoiceFilter, InvoicePatch](
        uc.MarkPaid,
        func(f InvoiceFilter, p InvoicePatch) BulkMarkPaidCommand {
            return BulkMarkPaidCommand{Filter: f, Patch: p}
        },
    ),
)

func NewJsonApiCommandHandler

func NewJsonApiCommandHandler[Cmd any, R resource.Resource, DTO resource.Resource](
	cmdFn func(ctx context.Context, cmd Cmd) (R, error),
	decoder func(*http.Request) (Cmd, error),
	encoder func(R) DTO,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiCommandHandler wires a write endpoint whose usecase method takes a typed Command struct rather than the resource directly. Fits the DDD pattern where the function signature carries the intent of the operation alongside its inputs:

func (uc *workspaceUsecase) Invite(ctx context.Context, cmd InviteMemberCommand) (Member, error)
func (uc *orderUsecase) Cancel(ctx context.Context, cmd CancelOrderCommand) (Order, error)

The decoder receives the *http.Request so it can read URL path params (req.PathValue("id")), query params, and body. Identity (actor, tenancy) is NEVER extracted by the decoder — middleware puts those on ctx and the usecase reads them when applying business rules.

The default success status is 201 Created. Override with HandlerWithCreatedStatus(http.StatusOK) for a Command that doesn't produce a new resource (e.g. POST /orders/{id}/cancel returns the updated order, not a new one).

func NewJsonApiCreateHandler

func NewJsonApiCreateHandler[R, DTO resource.Resource, C usecase.Creator[R]](
	creator C,
	decoder func(DTO) R, encoder func(res R) DTO,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiCreateHandler builds the POST handler for a jsonapi resource. When HandlerAllowsEmptyReq(true) is passed, the body decoder short-circuits on a zero-length body and feeds the creator a zero R — useful for token-driven creates (e.g. POST /v1/users bootstrapping from a Firebase token) where all inputs come from ctx and there is no meaningful body to send.

func NewJsonApiDeleteHandler

func NewJsonApiDeleteHandler(
	deleter usecase.Deleter, deleteType repository.DeleteType,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiDeleteHandler wires a DELETE endpoint. The deleteType (soft vs hard) is fixed per handler at registration time — usually soft, since hard-deletes are a privileged operation that goes through a separate code path.

func NewJsonApiGetHandler

func NewJsonApiGetHandler[R, DTO resource.Resource, C usecase.Getter[R]](
	getter C, encoder func(res R) DTO,
	parseOpts []query.ParseOpt,
	opts ...HandlerOpt,
) http.Handler

NewJsonApiGetHandler serves both the canonical "/{id}" Get and the singleton variant (e.g. /me, /config). Pass HandlerWithGetDecoderOpts(DecodeGetSkipURLPathID()) to skip the {id} path-segment requirement when identity comes from context instead.

func NewJsonApiListHandler

func NewJsonApiListHandler[R, DTO resource.Resource, C usecase.Lister[R]](
	lister C, resItemMapper func(res R) DTO, opts ...HandlerOpt,
) http.Handler

func NewJsonApiPatchHandler

func NewJsonApiPatchHandler[T, R, DTO resource.Resource, C usecase.Patcher[R]](
	patcher C, kind resource.Type,
	decoder func(T) []repository.PatchOption, encoder func(res R) DTO,
	opts ...HandlerOpt,
) http.Handler

func NewJsonApiUpdateHandler

func NewJsonApiUpdateHandler[R, DTO resource.Resource, C usecase.Updater[R]](
	updater C,
	decoder func(DTO) R, encoder func(res R) DTO,
	opts ...HandlerOpt,
) http.Handler

func NewListHandler

func NewListHandler[R resource.Resource, C usecase.Lister[R], O any](
	lister C, resItemMapper func(res R) O, opts ...HandlerOpt,
) http.Handler

func NewMultipartUploadHandler

func NewMultipartUploadHandler[Cmd any, R resource.Resource, DTO resource.Resource](
	cmdFn func(ctx context.Context, file io.Reader, cmd Cmd) (R, error),
	decoder func(file MultipartFile, req *http.Request) (Cmd, error),
	encoder func(R) DTO,
	opts ...UploadHandlerOpt,
) http.Handler

NewMultipartUploadHandler wires `POST /resources` whose body is a multipart/form-data document with one file part plus an arbitrary number of metadata fields. The decoder receives the parsed file + the *http.Request (path params, form values via req.FormValue) and returns a typed Command.

func decodeDocumentUpload(file rest.MultipartFile, req *http.Request) (UploadDocumentCommand, error) {
    return UploadDocumentCommand{
        WorkspaceID: req.PathValue("workspaceId"),
        FileName:    file.FileName,
        ContentType: file.ContentType,
    }, nil
}

r.Post("/workspaces/{workspaceId}/documents",
    kitrest.NewMultipartUploadHandler(
        uc.Upload, decodeDocumentUpload, api.DocumentToDTO))

The cmdFn receives the file reader alongside the Command; it streams bytes directly to storage without buffering. fileField is the form-field name for the file part (default "file" if empty).

func NewPatchHandler

func NewPatchHandler[T, R resource.Resource, C usecase.Patcher[R], O any](
	patcher C, kind resource.Type,
	decoder func(T) []repository.PatchOption, encoder func(res R) O,
	opts ...HandlerOpt,
) http.Handler

func NewResourceHandler

func NewResourceHandler[R resource.Resource, O any](
	e transport.Endpoint[R, R],
	decoder func(O) R, encoder func(res R) O,
	successCode int, opts ...HandlerOpt,
) http.Handler

func NewServer

func NewServer(opts ...serverOption) *http.Server

NewServer creates a new HTTP server with the given options.

Route registration is unified through a single root Router:

  • WithControllers(...) — each Controller's Routes(Router) method registers grouped routes + scoped middleware on the root router.
  • WithEndpoints(...) — singleton handlers (health, readiness, anything truly route-less). Registered at the root.

Global cfg.middlewares wrap the entire mux as the outermost layer, so they run before routing — important for RequestID, Tracing, Recovery which need to see 404s and 405s too. Ordering matches the Router's Use(): the first middleware in the slice is the outermost runtime layer (it runs first, returns last).

func NewServerSentEventsHandler

func NewServerSentEventsHandler[Cmd any](
	decoder func(*http.Request) (Cmd, error),
	producer func(ctx context.Context, cmd Cmd, out chan<- SSEEvent) error,
	opts ...SSEHandlerOpt,
) http.Handler

NewServerSentEventsHandler wires a GET endpoint that streams events to the client. The producer receives a context (cancelled when the client disconnects) and a channel to write events into; the handler frames each event per the SSE spec and writes a keep-alive comment every heartbeat interval.

r.Get("/signals/stream",
    kitrest.NewServerSentEventsHandler(
        func(req *http.Request) (StreamCommand, error) {
            return StreamCommand{Topic: req.URL.Query().Get("topic")}, nil
        },
        uc.Stream,
    ),
)

// usecase
func (uc *signalsUsecase) Stream(ctx context.Context, cmd StreamCommand, out chan<- rest.SSEEvent) error {
    sub := uc.bus.Subscribe(cmd.Topic)
    defer sub.Close()
    for {
        select {
        case <-ctx.Done(): return nil
        case sig := <-sub.C: out <- rest.SSEEvent{Event: "signal", Data: sig}
        }
    }
}

The handler closes the events channel when the producer returns or the client disconnects; the producer should not close it. Note that the http.Server's WriteTimeout must be 0 (disabled) for the route — otherwise long-lived streams are cut off. Wire with WithWriteTimeout(0) at server construction or behind a per-route opt-out if support for that lands.

func NewStreamingDownloadHandler

func NewStreamingDownloadHandler[Cmd any](
	cmdFn func(ctx context.Context, cmd Cmd) (*DownloadResponse, error),
	decoder func(*http.Request) (Cmd, error),
	opts ...HandlerOpt,
) http.Handler

NewStreamingDownloadHandler wires a GET endpoint whose response body is a raw byte stream (a document, an export, a generated report) — not a JSON:API document.

The usecase decodes path/query params into a typed Command, opens the underlying reader, and returns it; the handler is responsible for streaming, range support (`Accept-Ranges: bytes`, parsing the client's `Range:` header), and content headers.

r.Get("/documents/{id}/content",
    kitrest.NewStreamingDownloadHandler(
        uc.Download,
        func(req *http.Request) (DownloadCommand, error) {
            return DownloadCommand{ID: req.PathValue("id")}, nil
        },
    ),
)

func NewTracingTransport

func NewTracingTransport(monitor monitoring.Monitor, base http.RoundTripper) http.RoundTripper

NewTracingTransport returns an http.RoundTripper that injects the current span's trace context into outbound request headers. Wrap an existing transport (default http.DefaultTransport) with WithTransport. Use it via WithHTTPClient(&http.Client{Transport: NewTracingTransport(...)}).

func NewUpdateHandler

func NewUpdateHandler[R resource.Resource, C usecase.Updater[R], O any](
	updater C,
	decoder func(O) R, encoder func(res R) O,
	opts ...HandlerOpt,
) http.Handler

func PATCH

func PATCH[DO, EI any](ctx context.Context, c Client, path string, req EI) (DO, error)

func POST

func POST[DO, EI any](ctx context.Context, c Client, path string, req EI) (DO, error)

func PUT

func PUT[DO, EI any](ctx context.Context, c Client, path string, req EI) (DO, error)

func QueryOptsFromReq

func QueryOptsFromReq(parseOpts ...query.ParseOpt) func(ctx context.Context, r *http.Request) ([]query.Option, error)

func RequestIDFromContext

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext returns the request id installed by the RequestIDMiddleware, or "" if no middleware ran.

func RestJSONEncoder

func RestJSONEncoder[I, O any](
	itemMapper func(in I) O,
	successCode int,
) func(context.Context, http.ResponseWriter, any) error

func UnmarshalPayloadFromRequest

func UnmarshalPayloadFromRequest[DTO any](req *http.Request) (DTO, error)

UnmarshalPayloadFromRequest decodes a JSON:API document from the request body into DTO. Convenient one-liner for Command-handler decoders so they don't need to plumb `bytes.NewReader(io.ReadAll(req.Body))` or remember the kit error shape on a malformed body. Returns the decoded primary-data DTO; an InvalidArgument kit error on parse failure.

func decodeInviteMember(req *http.Request) (app.InviteMemberCommand, error) {
    body, err := kitrest.UnmarshalPayloadFromRequest[api.InviteMemberRequest](req)
    if err != nil {
        return app.InviteMemberCommand{}, err
    }
    return app.InviteMemberCommand{
        WorkspaceID: req.PathValue("id"),
        AccountID:   body.AccountID(),
        Role:        body.Role(),
    }, nil
}

func WithAddress

func WithAddress(address string) serverOption

WithAddress sets the address the inner *http.Server will listen to

func WithClientReqInterceptors

func WithClientReqInterceptors(reqInterceptors ...ClientRequestInterceptor) clientOption

func WithClientReqTimeout

func WithClientReqTimeout(timeout time.Duration) clientOption

func WithClientResInterceptors

func WithClientResInterceptors(resInterceptors ...ClientResponseInterceptor) clientOption

func WithControllers

func WithControllers(controllers ...Controller) serverOption

WithControllers adds Controllers whose Routes(Router) methods register grouped routes + scoped middleware on the server's root router.

func WithEndpoints

func WithEndpoints(endpoints ...Endpoint) serverOption

WithEndpoints adds the provided endpoints to the endpoint list

func WithErrorEncoder

func WithErrorEncoder(encoder ErrorEncoder) authMiddlewareOption

func WithHTTPClient

func WithHTTPClient(cli *http.Client) clientOption

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) serverOption

WithIdleTimeout caps how long an idle keep-alive connection can sit before the server closes it.

func WithJSONAPIIncludes

func WithJSONAPIIncludes(next http.Handler) http.Handler

WithJSONAPIIncludes is middleware that extracts the 'include' query parameter from the HTTP request and adds it to the context for later use by JSON:API encoders

func WithMaxConnections

func WithMaxConnections(n int) serverOption

WithMaxConnections caps the number of concurrent accepted connections. 0 disables the cap. Read by the fx module wrapper which wraps the listener with netutil.LimitListener.

func WithMaxHeaderBytes

func WithMaxHeaderBytes(n int) serverOption

WithMaxHeaderBytes overrides the maximum size of request headers.

func WithMiddlewares

func WithMiddlewares(middlewares ...Middleware) serverOption

WithMiddlewares adds the provided rest middlewares to the middleware list

func WithRateLimitIdleTTL added in v0.1.1

func WithRateLimitIdleTTL(d time.Duration) rateLimitOption

func WithRateLimitKeyFunc added in v0.1.1

func WithRateLimitKeyFunc(fn KeyFunc) rateLimitOption

func WithReadHeaderTimeout

func WithReadHeaderTimeout(d time.Duration) serverOption

WithReadHeaderTimeout caps the time a client may take to send the request line + headers. Defaults to defaultReadHeaderTimeout.

func WithReadTimeout

func WithReadTimeout(d time.Duration) serverOption

WithReadTimeout caps the time taken to read the full request body. Defaults to defaultReadTimeout. Set 0 to disable.

func WithShutdownTimeout

func WithShutdownTimeout(shutdownTimeout time.Duration) serverOption

WithShutdownTimeout sets the shutdown deadline

func WithTLSConfig

func WithTLSConfig(config *tls.Config) serverOption

WithTLSConfig sets the TLS configuration of the inner *http.Server

func WithTimeoutMessage added in v0.1.1

func WithTimeoutMessage(msg string) timeoutOption

WithTimeoutMessage overrides the body returned when the deadline fires.

func WithWriteTimeout

func WithWriteTimeout(d time.Duration) serverOption

WithWriteTimeout caps the time taken to write the response. Defaults to defaultWriteTimeout. Set 0 to disable — required for SSE, long-poll or any other streaming response.

func WriteJSONAPI

func WriteJSONAPI(w http.ResponseWriter, status int, model any) error

WriteJSONAPI marshals model as a single-resource jsonapi document and writes it to w with the given status. Uses the same buffer-first-then-flush pattern the kit handlers use so a marshal failure can still produce a clean JSON:API error (no torn response).

Exported as an escape hatch for handlers that don't fit Create/Get/List/Patch/Update/Delete or Command/Bulk shapes — rare, but real for things like multi-resource composite endpoints or webhook receivers that need to return a small ack.

func WriteJSONAPIError

func WriteJSONAPIError(w http.ResponseWriter, err error)

WriteJSONAPIError encodes err as a jsonapi error document and writes it to w with the appropriate status. Thin wrapper around JsonApiErrorEncoder for handlers that need to emit an error from outside the kit handler factories.

Types

type AccessLogMiddleware

type AccessLogMiddleware struct {
	// contains filtered or unexported fields
}

AccessLogMiddleware logs one line per request after the response is written. Place it close to the outside of the chain so duration covers the whole pipeline including auth + decoders.

func NewAccessLogMiddleware

func NewAccessLogMiddleware(monitor monitoring.Monitor) *AccessLogMiddleware

func (*AccessLogMiddleware) Intercept

func (m *AccessLogMiddleware) Intercept(next http.Handler) http.Handler

type AnyEndpoint

type AnyEndpoint func(ctx context.Context, request interface{}) (interface{}, error)

AnyEndpoint is a function that takes a request and returns a response

type AtomicOperation

type AtomicOperation struct {
	Op   string         `json:"op"`
	Ref  *AtomicRef     `json:"ref,omitempty"`
	Data map[string]any `json:"data,omitempty"`
}

AtomicOperation is one entry in a JSON:API Atomic Operations request body. Services that need true cross-resource atomicity wire a NewJsonApiAtomicOperationsHandler with a dispatcher that maps each operation to the right usecase call inside a single transaction.

See https://jsonapi.org/ext/atomic/ for the wire spec. We follow the spec's `op` field (add | update | remove); `data` carries the resource payload (for add / update) and the `ref` field can be used by callers to address an existing row by id.

type AtomicOperationResult

type AtomicOperationResult struct {
	Data map[string]any `json:"data,omitempty"`
}

AtomicOperationResult is the per-operation result returned to the client. The spec says the response is `atomic:results` with the same length and order as the request's `atomic:operations`.

type AtomicOperationsRequest

type AtomicOperationsRequest struct {
	Operations []AtomicOperation `json:"atomic:operations"`
}

AtomicOperationsRequest is the top-level envelope under the `urn:ietf:params:jsonapi:ext:atomic` extension namespace. The wire field is literally `atomic:operations`.

type AtomicOperationsResponse

type AtomicOperationsResponse struct {
	Results []AtomicOperationResult `json:"atomic:results"`
}

AtomicOperationsResponse is the top-level response envelope.

type AtomicRef

type AtomicRef struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}

AtomicRef points at an existing resource by type + id, used by the `update` and `remove` ops.

type AuthMiddleware

type AuthMiddleware struct {
	// contains filtered or unexported fields
}

func NewAuthMiddleware

func NewAuthMiddleware(authenticator HTTPAuthenticator, opts ...authMiddlewareOption) *AuthMiddleware

NewAuthMiddleware creates a Middleware that runs the supplied authenticator against every incoming request. On authentication failure the configured errorEncoder writes the error response and the inner handler is not invoked.

Wire it into a Controller's Routes via Router.Use:

r.Use(kitrest.NewAuthMiddleware(authenticator))

or scope it to a single registration with With():

r.With(kitrest.NewAuthMiddleware(authenticator)).Post("/", h)

func (*AuthMiddleware) Intercept

func (m *AuthMiddleware) Intercept(next http.Handler) http.Handler

Intercept implements the Middleware interface.

type BulkDeleteRequest

type BulkDeleteRequest[F any] struct {
	Filter F `json:"filter"`
}

BulkDeleteRequest is the canonical wire shape for `POST /resources:batchDelete`. Filter-only; no patch.

type BulkDeleteResponse

type BulkDeleteResponse struct {
	resource.RestDTO
	RAffectedCount int `jsonapi:"attr,affectedCount"`
}

BulkDeleteResponse mirrors BulkUpdateResponse.

type BulkUpdateRequest

type BulkUpdateRequest[F any, P any] struct {
	Filter F `json:"filter"`
	Patch  P `json:"patch"`
}

BulkUpdateRequest is the canonical wire shape for `POST /resources:batchUpdate`: a filter (server-sided selector) plus a patch to apply. Services consume `BulkUpdateRequest[F, P]` via a Command-style decoder that builds the usecase command.

The kit doesn't impose a Filter or Patch struct shape — that lives in each service's app layer. F and P are whatever the service defines; this struct just nails down the envelope on the wire.

type BulkUpdateResponse

type BulkUpdateResponse struct {
	resource.RestDTO
	RAffectedCount int `jsonapi:"attr,affectedCount"`
}

BulkUpdateResponse reports how many rows the bulk update touched. Services that need richer responses (per-row results, IDs of updated rows) should compose a custom Command handler instead.

type CORSMiddleware

type CORSMiddleware struct {
	// contains filtered or unexported fields
}

CORSMiddleware provides Cross-Origin Resource Sharing support.

Per the Fetch spec the response must include `Vary: Origin` on every cross-origin response so caches keyed on the request URL alone do not serve the wrong Access-Control-Allow-Origin to a different client. The header is emitted on every response from this middleware regardless of whether the request itself was cross-origin — a cached non-cross-origin response that later gets reused by a cross-origin client would otherwise inherit the wrong CORS headers.

func (*CORSMiddleware) Intercept

func (m *CORSMiddleware) Intercept(next http.Handler) http.Handler

Intercept implements the Middleware interface

type Checker added in v0.1.1

type Checker func(ctx context.Context) error

Checker reports whether a subsystem is healthy. Implementations should respect ctx (which the aggregator gives a short timeout) and return a descriptive error on failure.

type Client

type Client interface {
	Call(ctx context.Context, method, path string, req any) (any, error)
}

type ClientEndpoint

type ClientEndpoint interface {
	HttpClient() *http.Client
	Target() *url.URL
	Path() string
	Method() string
	Encode(context.Context, *http.Request, any) error
	Decode(context.Context, *http.Response) (response any, err error)
	ReqInterceptors() []ClientRequestInterceptor
	ResInterceptors() []ClientResponseInterceptor
	ErrInterceptors() []ClientErrorInterceptor
	Endpoint() AnyEndpoint
}

func NewDELETE

func NewDELETE[EI, DO any](path string,
	enc func(context.Context, *http.Request, EI) error,
	dec func(context.Context, *http.Response) (response DO, err error),
	opts ...ClientEndpointOpt,
) (ClientEndpoint, error)

func NewGET

func NewGET[EI, DO any](path string,
	enc func(context.Context, *http.Request, EI) error,
	dec func(context.Context, *http.Response) (response DO, err error),
	opts ...ClientEndpointOpt,
) (ClientEndpoint, error)

func NewPATCH

func NewPATCH[EI, DO any](path string,
	enc func(context.Context, *http.Request, EI) error,
	dec func(context.Context, *http.Response) (response DO, err error),
	opts ...ClientEndpointOpt,
) (ClientEndpoint, error)

func NewPOST

func NewPOST[EI, DO any](path string,
	enc func(context.Context, *http.Request, EI) error,
	dec func(context.Context, *http.Response) (response DO, err error),
	opts ...ClientEndpointOpt,
) (ClientEndpoint, error)

func NewPUT

func NewPUT[EI, DO any](path string,
	enc func(context.Context, *http.Request, EI) error,
	dec func(context.Context, *http.Response) (response DO, err error),
	opts ...ClientEndpointOpt,
) (ClientEndpoint, error)

type ClientEndpointOpt

type ClientEndpointOpt func(c *clientEndpoint)

func WithClientEndpointErrInterceptors

func WithClientEndpointErrInterceptors(errInterceptors ...ClientErrorInterceptor) ClientEndpointOpt

func WithClientEndpointHttpClient

func WithClientEndpointHttpClient(client *http.Client) ClientEndpointOpt

func WithClientEndpointReqInterceptors

func WithClientEndpointReqInterceptors(reqInterceptors ...ClientRequestInterceptor) ClientEndpointOpt

func WithClientEndpointResInterceptors

func WithClientEndpointResInterceptors(resInterceptors ...ClientResponseInterceptor) ClientEndpointOpt

func WithClientEndpointTarget

func WithClientEndpointTarget(target *url.URL) ClientEndpointOpt

type ClientErrorInterceptor

type ClientErrorInterceptor func(context.Context, error)

type ClientRequestInterceptor

type ClientRequestInterceptor func(context.Context, *http.Request) context.Context

func ClientRequestWithBearer

func ClientRequestWithBearer(token string) ClientRequestInterceptor

func ClientRequestWithContentType

func ClientRequestWithContentType(mime string) ClientRequestInterceptor

func ClientRequestWithHeaders

func ClientRequestWithHeaders(kvs ...string) ClientRequestInterceptor

func ClientRequestWithQueryParams

func ClientRequestWithQueryParams(qParams url.Values) ClientRequestInterceptor

type ClientResponseInterceptor

type ClientResponseInterceptor func(context.Context, *http.Response) context.Context

type Collector

type Collector struct {
	BasePath string // URL path to docs, default "/docs".

	// CombineErrors can take a value of "oneOf" or "anyOf",
	// if not empty it enables logical schema grouping in case
	// of multiple responses with same HTTP status code.
	CombineErrors string

	// DefaultSuccessResponseContentType is a default success response content type.
	// If empty, "application/json" is used.
	DefaultSuccessResponseContentType string

	// DefaultErrorResponseContentType is a default error response content type.
	// If empty, "application/json" is used.
	DefaultErrorResponseContentType string

	// DefaultMethods list is used when handler serves all methods.
	DefaultMethods []string

	// OperationExtractor allows flexible extraction of OpenAPI information.
	OperationExtractor func(h http.Handler) func(oc openapi.OperationContext) error

	// Host filters routes by host, gorilla/mux can serve different handlers at
	// same method, paths with different hosts. This can not be expressed with a single
	// OpenAPI document.
	Host string
	// contains filtered or unexported fields
}

Collector extracts OpenAPI documentation from HTTP handler and underlying use case interactor.

func NewCollector

func NewCollector(r openapi.Reflector) *Collector

NewCollector creates an instance of OpenAPI Collector.

func (*Collector) AnnotateOperation

func (c *Collector) AnnotateOperation(method, pattern string, setup ...func(oc openapi.OperationContext) error)

AnnotateOperation adds OpenAPI operation configuration that is applied during collection, method can be empty to indicate any method.

func (*Collector) CollectOperation

func (c *Collector) CollectOperation(
	method, pattern string,
	annotations ...func(oc openapi.OperationContext) error,
) (err error)

CollectOperation prepares and adds OpenAPI operation.

func (*Collector) HasAnnotation

func (c *Collector) HasAnnotation(method, pattern string) bool

HasAnnotation indicates if there is at least one annotation registered for this operation.

func (*Collector) Refl

func (c *Collector) Refl() openapi.Reflector

Refl returns OpenAPI reflector.

func (*Collector) Reflector

func (c *Collector) Reflector() *openapi3.Reflector

Reflector is an accessor to OpenAPI Reflector instance.

func (*Collector) ServeHTTP

func (c *Collector) ServeHTTP(rw http.ResponseWriter, _ *http.Request)

func (*Collector) SpecSchema

func (c *Collector) SpecSchema() openapi.SpecSchema

SpecSchema returns OpenAPI specification schema.

type ContextTimeoutMiddleware added in v0.1.1

type ContextTimeoutMiddleware struct {
	// contains filtered or unexported fields
}

ContextTimeoutMiddleware is a lighter alternative: it cancels the request context after the timeout but does NOT block the response writer. Handlers that respect ctx.Done() will return early; others run to completion. Use this when you want timeouts to control downstream calls (DB, RPC) but don't want a blocked response writer.

func NewContextTimeoutMiddleware added in v0.1.1

func NewContextTimeoutMiddleware(timeout time.Duration) *ContextTimeoutMiddleware

func (*ContextTimeoutMiddleware) Intercept added in v0.1.1

func (m *ContextTimeoutMiddleware) Intercept(next http.Handler) http.Handler

type Controller

type Controller interface {
	Routes(r Router)
}

Controller declares HTTP routes for an aggregate. The Routes method receives a Router and registers grouped routes + scoped middleware on it. fx-wired controllers are collected via NewFxController and invoked at NewServer time against the server's root router.

func (c *workspaceController) Routes(r rest.Router) {
    r.Route("/v1/workspaces", func(r rest.Router) {
        r.Use(c.auth, c.actor)
        r.Post("/", c.create)
        r.Route("/{id}", func(r rest.Router) {
            r.Use(c.tenancy)
            r.Delete("/", c.delete)
        })
    })
}

type DecodeRequestFunc

type DecodeRequestFunc func(context.Context, *http.Request) (request interface{}, err error)

type DownloadResponse

type DownloadResponse struct {
	ContentType string
	Size        int64
	// Filename, when non-empty, populates the
	// `Content-Disposition: attachment; filename="..."` header so
	// browsers offer a save-as dialog.
	Filename string
	// Body is the reader for the response bytes. Closed by the
	// handler after the body is fully written (or on range error).
	Body io.ReadCloser
}

DownloadResponse is what a streaming-download usecase returns. The kit handler writes Content-Type / Content-Length / Content-Disposition off this struct, sets Accept-Ranges, parses Range: requests, and streams the body. Size is required so HEAD requests and Range validation can do their thing.

type EncodeResponseFunc

type EncodeResponseFunc func(context.Context, http.ResponseWriter, interface{}) error

func NewEmptyHTTPEncoder

func NewEmptyHTTPEncoder(statusCode int) EncodeResponseFunc

NewEmptyHTTPEncoder writes only the status code, no body. Safe without the writeBuffered() guard that the data-bearing encoders use — there's no marshalling step that could fail half-way and leave a torn response.

func NewHTTPEncoder

func NewHTTPEncoder[I any](
	f func(context.Context, http.ResponseWriter, I) error,
	opts ...encoderOpt,
) EncodeResponseFunc

type Endpoint

type Endpoint interface {
	Method() string
	Path() string
	http.Handler
}

Endpoint is a (method, path, handler) triple used by WithEndpoints to register singleton routes that don't belong to a Controller — /healthz, /readyz, or any handler attached at the server level.

func NewEndpoint

func NewEndpoint(method, path string, handler http.Handler) Endpoint

func NewHealthEndpoint

func NewHealthEndpoint() Endpoint

func ReadinessEndpoint

func ReadinessEndpoint(r *Readiness) Endpoint

ReadinessEndpoint constructs the /readyz Endpoint to register on the REST server.

type ErrorEncoder

type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter)

type FxConfig

type FxConfig struct {
	HTTPAddress string `required:"true" envconfig:"HTTP_ADDRESS"`
}

type GetDecoderOpt

type GetDecoderOpt func(c *getEncoderConfig)

GetDecoderOpt configures the Get-request decoder used by NewJsonApiGetHandler. Plumbed through HandlerWithGetDecoderOpts so callers can compose options at the handler-factory level.

func DecodeGetSkipURLPathID

func DecodeGetSkipURLPathID() GetDecoderOpt

DecodeGetSkipURLPathID disables the "{id}" path-segment requirement in the Get-request decoder. Used by singleton endpoints (/me, /config, /session) where identity is resolved from the context rather than passed as a path id.

type HTTPAuthenticator

type HTTPAuthenticator interface {
	Authenticate(req *http.Request) error
}

type HandlerOpt

type HandlerOpt func(c *handlerConfig)

func HandlerAllowsEmptyReq

func HandlerAllowsEmptyReq(emptyReqAllowed bool) HandlerOpt

func HandlerWithErrorEncoder

func HandlerWithErrorEncoder(ee ErrorEncoder) HandlerOpt

func HandlerWithGetDecoderOpts

func HandlerWithGetDecoderOpts(opts ...GetDecoderOpt) HandlerOpt

HandlerWithGetDecoderOpts threads decoder options through the jsonapi Get-handler factory. The canonical use case is opting a singleton endpoint (/me, /config) out of the {id} path-segment requirement:

r.Get("/me",
    kitrest.NewJsonApiGetHandler(uc, api.UserToDTO, nil,
        kitrest.HandlerWithGetDecoderOpts(kitrest.DecodeGetSkipURLPathID()),
    ),
)

func HandlerWithSuccessStatus

func HandlerWithSuccessStatus(code int) HandlerOpt

HandlerWithSuccessStatus overrides the success status code for handlers that support it (currently Command). Defaults vary per factory (Create = 201, Get/List/Update/Patch = 200, Delete = 204); Command defaults to 201 but a "operation on existing resource" command (POST /orders/{id}/cancel) often wants 200.

type Headerer

type Headerer interface {
	Headers() http.Header
}

Headerer is checked by DefaultErrorEncoder. If an error value implements Headerer, the provided headers will be applied to the response writer, after the Content-Type is set.

type KeyFunc added in v0.1.1

type KeyFunc func(*http.Request) string

KeyFunc derives the rate-limit bucket key from a request. Defaults to the client IP (X-Forwarded-For first, then RemoteAddr).

type Middleware

type Middleware interface {
	Intercept(http.Handler) http.Handler
}

Middleware wraps an http.Handler with cross-cutting behaviour (authentication, logging, recovery, tracing, ...).

func DefaultObservabilityMiddlewares

func DefaultObservabilityMiddlewares(monitor monitoring.Monitor) []Middleware

DefaultObservabilityMiddlewares returns the recommended middleware stack (outermost first): RequestID → AccessLog → Tracing → Recovery. Apply with WithMiddlewares(...).

func NewCORSMiddleware

func NewCORSMiddleware() Middleware

NewCORSMiddleware creates a new CORS middleware that accepts any origin.

func NewCORSMiddlewareWithOrigins

func NewCORSMiddlewareWithOrigins(origins []string) Middleware

NewCORSMiddlewareWithOrigins creates a CORS middleware with specific allowed origins. Cross-origin requests from origins not in the list receive a response with NO Access-Control-Allow-Origin header — the browser then blocks the request. The previous implementation echoed `m.allowedOrigins[0]` for disallowed origins, which silently granted CORS access to the wrong domain.

type MiddlewareFunc

type MiddlewareFunc func(http.Handler) http.Handler

MiddlewareFunc is the adapter that lets a plain function satisfy the Middleware interface.

func (MiddlewareFunc) Intercept

func (f MiddlewareFunc) Intercept(h http.Handler) http.Handler

type MultipartFile

type MultipartFile struct {
	FileName    string
	ContentType string
	Size        int64 // -1 when the multipart part doesn't declare a length
	Reader      io.Reader
}

MultipartFile holds the file part extracted from a multipart/form-data request. The reader is owned by the multipart.Reader; the handler closes it after the usecase returns.

type OpenAPIPreparer

type OpenAPIPreparer interface {
	SetupOpenAPIOperation(oc openapi.OperationContext) error
}

OpenAPIPreparer defines http.Handler with OpenAPI information.

type ProbeOption added in v0.1.1

type ProbeOption func(*Probes)

func WithManualReadinessToggle added in v0.1.1

func WithManualReadinessToggle(r *Readiness) ProbeOption

WithManualReadinessToggle wires in an existing Readiness toggle. When set, readiness reports "not ready" while the toggle is off, regardless of the registered checks. Use this for graceful shutdown draining.

func WithProbeCheckTimeout added in v0.1.1

func WithProbeCheckTimeout(d time.Duration) ProbeOption

WithProbeCheckTimeout sets the per-check deadline. Default: 1s.

func WithProbeOverallTimeout added in v0.1.1

func WithProbeOverallTimeout(d time.Duration) ProbeOption

WithProbeOverallTimeout sets the total budget for a single probe request. Default: 3s.

type Probes added in v0.1.1

type Probes struct {
	// contains filtered or unexported fields
}

Probes aggregates named liveness + readiness checks into HTTP handlers. Mount the handlers at /healthz (liveness) and /readyz (readiness) — a failed readiness check stops a load balancer from sending new traffic, a failed liveness check tells the orchestrator to restart the process.

probes := rest.NewProbes()
probes.AddReadiness("db", func(ctx context.Context) error { return db.PingContext(ctx) })
probes.AddLiveness("memory", checkers.MemoryHealthy)
router.Mount("/healthz", probes.LivenessHandler())
router.Mount("/readyz", probes.ReadinessHandler())

func NewProbes added in v0.1.1

func NewProbes(opts ...ProbeOption) *Probes

func (*Probes) AddLiveness added in v0.1.1

func (p *Probes) AddLiveness(name string, c Checker)

func (*Probes) AddReadiness added in v0.1.1

func (p *Probes) AddReadiness(name string, c Checker)

func (*Probes) LivenessEndpoint added in v0.1.1

func (p *Probes) LivenessEndpoint() Endpoint

LivenessEndpoint constructs the /healthz Endpoint for the REST server.

func (*Probes) LivenessHandler added in v0.1.1

func (p *Probes) LivenessHandler() http.HandlerFunc

LivenessHandler runs registered liveness checks. 200 when all pass, 503 when any fail or time out.

func (*Probes) ReadinessEndpoint added in v0.1.1

func (p *Probes) ReadinessEndpoint() Endpoint

ReadinessEndpoint constructs the /readyz Endpoint for the REST server.

func (*Probes) ReadinessHandler added in v0.1.1

func (p *Probes) ReadinessHandler() http.HandlerFunc

ReadinessHandler runs registered readiness checks. 200 when all pass, 503 when any fail, time out, or the manual toggle is off.

type RateLimitMiddleware added in v0.1.1

type RateLimitMiddleware struct {
	// contains filtered or unexported fields
}

RateLimitMiddleware caps requests per key using a token-bucket per bucket. Buckets are GC'd after idle to keep the map bounded.

func NewRateLimitMiddleware added in v0.1.1

func NewRateLimitMiddleware(rps float64, burst int, opts ...rateLimitOption) *RateLimitMiddleware

NewRateLimitMiddleware builds a per-key token bucket. `rps` is the steady refill rate; `burst` is the max queued tokens. Defaults: key by IP, GC idle buckets after 10 minutes.

func (*RateLimitMiddleware) Intercept added in v0.1.1

func (m *RateLimitMiddleware) Intercept(next http.Handler) http.Handler

type Readiness

type Readiness struct {
	// contains filtered or unexported fields
}

Readiness is a thread-safe ready/not-ready toggle backed by an HTTP handler. The fx module installs one as /readyz, registers it for /readyz, starts it as ready, then flips it to not-ready at the top of the shutdown hook so load balancers stop sending new traffic before the in-flight requests finish draining.

func NewReadiness

func NewReadiness() *Readiness

NewReadiness returns a Readiness initialised to ready=true.

func (*Readiness) Handler

func (r *Readiness) Handler() http.HandlerFunc

Handler returns the HTTP handler — 200 when ready, 503 when not.

func (*Readiness) IsReady

func (r *Readiness) IsReady() bool

IsReady reports the current state.

func (*Readiness) SetReady

func (r *Readiness) SetReady(ready bool)

SetReady flips the readiness state.

type RecoveryMiddleware

type RecoveryMiddleware struct {
	// contains filtered or unexported fields
}

RecoveryMiddleware turns handler panics into 500 responses and records the panic on the active span. Without it a panicking handler kills only the goroutine but leaves the client hanging until ReadTimeout.

func NewRecoveryMiddleware

func NewRecoveryMiddleware(monitor monitoring.Monitor) *RecoveryMiddleware

func (*RecoveryMiddleware) Intercept

func (m *RecoveryMiddleware) Intercept(next http.Handler) http.Handler

type RequestIDMiddleware

type RequestIDMiddleware struct{}

RequestIDMiddleware ensures every request has an id, propagates it through ctx and into the response header. Pairs with the logger and tracer middlewares so log lines and spans share the same correlation id.

func NewRequestIDMiddleware

func NewRequestIDMiddleware() *RequestIDMiddleware

func (*RequestIDMiddleware) Intercept

func (m *RequestIDMiddleware) Intercept(next http.Handler) http.Handler

type Router

type Router interface {
	// Route mounts a sub-router at prefix. The fn receives the
	// sub-router and registers its own routes / nested groups /
	// middleware against it; nothing leaks out to the parent except
	// the registered routes themselves.
	Route(prefix string, fn func(Router))

	// With returns a sub-router whose subsequent registrations are
	// wrapped with the given middleware, scoped just to the returned
	// router. Doesn't mutate the parent. Idiom: r.With(authMW).Post(...).
	With(mw ...Middleware) Router

	// Use adds middleware cumulatively to the current router's scope.
	// All subsequent registrations on this router (and any nested
	// Route()) inherit it.
	Use(mw ...Middleware)

	// Per-method handler registration. Path is joined onto the
	// current scope's prefix.
	Get(path string, h http.Handler)
	Post(path string, h http.Handler)
	Put(path string, h http.Handler)
	Patch(path string, h http.Handler)
	Delete(path string, h http.Handler)

	// Method is the generic registration helper. Use for non-standard
	// verbs (HEAD, OPTIONS) when you really need them.
	Method(method, path string, h http.Handler)

	// Mount attaches an arbitrary http.Handler at prefix. The handler
	// receives requests with the prefix stripped (via
	// http.StripPrefix). Useful for sub-services, file servers, or a
	// third-party router we want to delegate to.
	Mount(prefix string, h http.Handler)
}

Router is the registration surface for HTTP routes. A Controller receives a Router and declares endpoints via Get/Post/Put/Patch/ Delete/Method, grouping them by URL prefix with Route(), stacking middleware with Use() (cumulative for the scope) or With() (one-off for a single registration chain).

The Router collects registrations during the boot phase; at server build time the tree is flattened into a single http.ServeMux. There is no runtime routing layer beyond what stdlib provides — the Router is just an accumulator that knows how to compose path prefixes and middleware stacks before handing off to mux.Handle.

Path conventions:

  • prefix on Route() and path on Get/Post/... are joined naively with "/"; leading/trailing slashes are normalised so callers don't have to think about it
  • stdlib mux path params ({id}, {key...}) flow through unchanged
  • the empty path "" on a leaf method (e.g. r.Get("", h)) means "the bare prefix" — handy inside a Route("/{id}", ...) block

Middleware ordering: the first middleware in a Use(a, b, c) call is the OUTERMOST (runs first, returns last). Chained scopes concatenate root-to-leaf: a route registered inside Route("/a", func(r){ r.Use(mwA); r.Route("/b", func(r){ r.Use(mwB); r.Get("/c", h) }) }) is wrapped as mwA(mwB(h)) — outer scope's middleware is outermost.

func NewRouter

func NewRouter() Router

NewRouter returns a fresh top-level Router. Callers usually don't construct one directly — NewServer builds the root Router, passes it to each Controller's Routes() method, and flattens the result into the mux.

type SSEEvent

type SSEEvent struct {
	ID    string
	Event string
	Data  any
}

SSEEvent is one frame sent to the client. ID is optional; clients reuse the last seen id when reconnecting via the `Last-Event-ID` header. Event is the optional event name (clients listen via `eventSource.addEventListener("name", ...)`); Data is the payload.

type SSEHandlerOpt

type SSEHandlerOpt func(*sseConfig)

SSEHandlerOpt configures NewServerSentEventsHandler.

func SSEWithHeartbeatInterval

func SSEWithHeartbeatInterval(d time.Duration) SSEHandlerOpt

SSEWithHeartbeatInterval overrides the keep-alive comment cadence (default 25s). Set 0 to disable heartbeats entirely.

type StatusCoder

type StatusCoder interface {
	StatusCode() int
}

StatusCoder is checked by DefaultErrorEncoder. If an error value implements StatusCoder, the StatusCode will be used when encoding the error. By default, StatusInternalServerError (500) is used.

type TimeoutMiddleware added in v0.1.1

type TimeoutMiddleware struct {
	// contains filtered or unexported fields
}

TimeoutMiddleware enforces a per-request deadline. When the handler hasn't finished by the time limit, the request context is cancelled — handlers respecting ctx.Done() will return, and the client sees 503 Service Unavailable.

func NewTimeoutMiddleware added in v0.1.1

func NewTimeoutMiddleware(timeout time.Duration, opts ...timeoutOption) *TimeoutMiddleware

func (*TimeoutMiddleware) Intercept added in v0.1.1

func (m *TimeoutMiddleware) Intercept(next http.Handler) http.Handler

type TracingMiddleware

type TracingMiddleware struct {
	// contains filtered or unexported fields
}

TracingMiddleware extracts trace context from request headers, starts a server span around the handler, and records status + error attributes.

func NewTracingMiddleware

func NewTracingMiddleware(monitor monitoring.Monitor) *TracingMiddleware

func (*TracingMiddleware) Intercept

func (m *TracingMiddleware) Intercept(next http.Handler) http.Handler

type UploadHandlerOpt

type UploadHandlerOpt func(*uploadConfig)

UploadHandlerOpt configures NewMultipartUploadHandler. Options cover the field name + the multipart memory cap.

func UploadWithFileField

func UploadWithFileField(name string) UploadHandlerOpt

UploadWithFileField overrides the form-field name for the file part (default "file"). Useful when integrating with a client that uses a non-standard field name.

func UploadWithMaxMemory

func UploadWithMaxMemory(bytes int64) UploadHandlerOpt

UploadWithMaxMemory caps the in-memory portion of the multipart parser. Set higher for endpoints that accept many small fields, lower to push spooling to /tmp sooner.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL