Browse all documentation

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:

content/orders.go
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:

content/order-endpoint.go
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:

content/config.go
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:

tracking.ts
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.