Custom endpoints
Add root, collection, and global HTTP handlers with generated OpenAPI metadata.
Custom endpoints add application-specific HTTP behavior beside Ridu’s generated REST API. Declare
them on ridu.Config, a ridu.Collection, or a ridu.Global; Ridu keeps their public method, path,
and summary in the v1 manifest and generated OpenAPI document while the compiled Go handler stays
runtime-only.
Mounting and paths
The config location determines the route prefix:
| Declaration | Mounted route |
|---|---|
ridu.Config.Endpoints |
/api<path> |
ridu.Collection.Endpoints |
/api/collections/<collection-slug><path> |
ridu.Global.Endpoints |
/api/globals/<global-slug><path> |
Path starts with /. A complete segment beginning with : captures one decoded value, so
/:id/tracking exposes id. Configured static segments and parameter names cannot contain
whitespace, percent escapes, traversal, query strings, fragments, partial parameters, or empty
segments. A path of / targets the scope root, and requests may include one trailing slash. Root
endpoints require a static first segment and cannot enter the reserved /collections/… or
/globals/… namespaces; declare those handlers on the resource.
Captured values must be non-empty and cannot decode to / or \. When multiple methods share a
path shape, use the same parameter name in each declaration so the generated OpenAPI path remains
unambiguous. Supported methods are CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST,
and PUT; config resolution accepts any case and stores uppercase metadata.
Collection and global endpoint matching stays inside its known resource scope. Within one scope,
custom endpoints run before a built-in route for the same method. This supports a wrapper or
replacement such as GET /count; a custom endpoint registered for a different method
does not hide the built-in method.
Add a collection endpoint
This Payload-familiar tracking route is available at
GET /api/collections/orders/<id>/tracking:
package content
import (
"encoding/json"
"net/http"
"github.com/riducms/ridu"
"github.com/riducms/ridu/field"
)
var Orders = ridu.Collection{
Slug: "orders",
Fields: []field.Definition{
field.Text("reference", field.Required()),
},
Endpoints: []ridu.Endpoint{{
Method: http.MethodGet,
Path: "/:id/tracking",
Summary: "Read order tracking",
Handler: func(ctx ridu.EndpointContext) {
if ctx.Actor == nil {
http.Error(ctx.Writer, "authentication required", http.StatusUnauthorized)
return
}
id := ctx.RouteParams["id"]
// The same value is also available through ctx.Request.PathValue("id").
ctx.Writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(ctx.Writer).Encode(map[string]string{
"id": id,
"status": "in-transit",
})
},
}},
}The typed Collection or Global field identifies the owning scope without parsing the URL.
Use the authenticated local API
EndpointContext.Local is the application’s Local API. Pass both actor fields into
options so collection/global access, validation, hooks, transactions, and field redaction remain
authoritative:
order, err := ctx.Local.FindWithOptions(
ctx.Request.Context(),
"orders",
ctx.RouteParams["id"],
ridu.FindOptions{
Actor: ctx.Actor,
ActorCollection: ctx.ActorCollection,
},
)
if err != nil {
// Translate the operation failure to the endpoint's chosen public response.
http.Error(ctx.Writer, "order unavailable", http.StatusNotFound)
return
}The endpoint chooses its own response envelope. Return JSON, text, an empty response, or a bounded stream as the route contract requires. Ridu still applies host and origin checks, CORS preflight, request IDs and deadlines, identity resolution, panic recovery, and error reporting.
Root and global endpoints
Root endpoints belong on the top-level config:
return ridu.Config{
Name: "Acme Editorial",
Endpoints: []ridu.Endpoint{{
Method: http.MethodPost,
Path: "/revalidate/:site",
Summary: "Revalidate a frontend site",
Handler: revalidateSite,
}},
Collections: []ridu.Collection{Orders},
Globals: []ridu.Global{SiteSettings},
}That route mounts at POST /api/revalidate/:site. A global declaration with
Path: "/refresh" on site-settings mounts at
/api/globals/site-settings/refresh.
Body limits, cancellation, and errors
MaxBodyBytes: 0 inherits ridu.HandlerOptions.MaxBodyBytes (1 MiB by default). A positive value
sets a tighter route limit through http.MaxBytesReader. A negative value opts trusted streaming
code out of that byte limit; pair it with explicit work, duration, and downstream bounds.
Use ctx.Request.Context() for every dependency call. It carries client cancellation and the
framework request deadline. ctx.RequestID matches the X-Request-ID response header.
Call ctx.ReportError(err, "stable_code") when the endpoint writes its own public failure response
but an internal dependency error should reach HandlerOptions.RequestError and request
observations. The trusted error is never copied into Ridu’s panic response. An authentication-like
endpoint should call ctx.AdmitAuthAttempt before work that aliases or repeated requests can
amplify.
Call a custom endpoint from TypeScript
Custom endpoints own arbitrary request and response shapes, so the SDK exposes raw Fetch rather than pretending they share collection envelopes:
const response = await ridu.request(`/api/collections/orders/${encodeURIComponent(id)}/tracking`, {
method: 'GET'
});
if (!response.ok) throw new Error(`tracking failed: ${response.status}`);
const tracking = (await response.json()) as { id: string; status: string };request accepts only a same-origin absolute-path reference. It retains configured credentials,
default and per-call headers, middleware, AbortSignal, and keepalive, but returns the raw
Response for every status and does not add a content type or parse an envelope. Use it for custom
endpoints; keep ordinary content work on the generated typed methods.
OpenAPI and generation
The v1 manifest contains each custom endpoint’s uppercase method, Payload-style path, and optional
summary. Generation converts named segments to OpenAPI path parameters, for example
/:id/tracking to /{id}/tracking. The request and response schemas remain endpoint-owned, so the
generated operation documents unconstrained 2XX and default responses. OpenAPI has no standard
CONNECT Path Item operation, so Ridu records that method under the valid x-ridu-connect vendor
extension. With ridu dev running, saving the config regenerates
generated/ridu.openapi.json; inspect and commit that file. Use ridu generate only when you need
the same update as a one-shot command, such as in CI.
Executable handlers, body-limit choices, actor data, and secrets never enter the manifest. Invalid methods, paths, duplicates within one scope, or missing handlers fail config resolution before the server starts.