Custom Endpoints and Idempotency
FRL endpoints let a theme expose small JSON routes for workflows that do not fit a normal uploadPost, updatePost, or deletePost call.
Use them for checks, calculators, action buttons, callback-style workflows, and controlled operations where the rule file should decide what is accepted and what response is returned.
Basic Endpoint
endpoint.post createLead("/leads/submit") {
verify $request.body.email && $request.body.acceptedTerms
idempotency `${str.lower(str.trim($request.body.email))}:${$request.body.campaign || "default"}`
process {
$context.email = str.lower(str.trim($request.body.email));
return {
ok: true,
email: $context.email,
message: "Lead received"
};
}
}
Call it from the theme with normal browser JavaScript:
const response = await fetch("/fb-endpoints/leads/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "Ada@Example.com",
campaign: "summer",
acceptedTerms: true
})
});
const result = await response.json();
The browser receives the object returned by the endpoint process block. If the returned value is not an object, Frontbacked wraps it as { ok: true, result }.
Endpoint Syntax
endpoint.method endpointName("/path/:param") {
verify expression
idempotency expression
process {
return { ok: true };
}
}
| Part | Purpose |
|---|---|
method | One of get, post, put, or delete. |
endpointName | A unique name for this endpoint in the rule file. |
"/path/:param" | Public path under /fb-endpoints. Params can use :id or {id}. |
verify | Optional guard evaluated before process. Return truthy to continue. |
idempotency | Optional stable key that makes repeated matching requests safe. |
process | Endpoint body. It can assign $context, call helpers, and return a JSON response. |
You can also document the endpoint with comments directly above it. Frontbacked uses those comments when showing endpoint metadata in admin tools.
// Receives newsletter signups from the public landing page.
endpoint.post newsletterSignup("/newsletter") {
verify $request.body.email
process {
return { ok: true };
}
}
Request Data
Endpoint rules read request data through $request.
| Value | Example |
|---|---|
| Method | $request.method |
| Matched path | $request.path |
| Declared endpoint path | $request.endpointPath |
| Path params | $request.params.slug |
| Query params | $request.query.plan |
| Parsed body | $request.body.email |
| Raw body text | $request.rawBody |
| Headers | $request.headers["content-type"] |
Other familiar FRL values are also available: $user, $visitor, $admin, $wallet, $secret, $private, $page, $currency, $context, and local fn helpers.
Path Params
endpoint.get checkUsername("/username/:username") {
verify $request.params.username
process {
$context.username = str.lower(str.trim($request.params.username));
return {
ok: true,
username: $context.username
};
}
}
The route /fb-endpoints/username/Ada sets $request.params.username to "Ada".
Idempotency
Use idempotency when a request may be repeated by the browser, a user double-click, a network retry, or an external callback.
endpoint.post confirmOrder("/orders/confirm") {
verify $request.body.orderId && $request.body.reference
idempotency `${$request.body.orderId}:${$request.body.reference}`
process {
return {
ok: true,
orderId: $request.body.orderId
};
}
}
With an idempotency key:
| Situation | Result |
|---|---|
| First request for a key | The endpoint runs normally. |
| Same key, same request, already completed | Frontbacked returns the saved response. |
| Same key, same request, still running | Frontbacked returns a processing response. |
| Same key, different request body/query/path | Frontbacked rejects it as a conflict. |
Good keys are deterministic and scoped to the real-world action:
idempotency `${$user.id}:${$request.body.invoiceId}:${$request.body.reference}`
idempotency `${str.lower(str.trim($request.body.email))}:${$request.body.campaign}`
Avoid random keys for idempotent endpoints:
// Do not do this for idempotency.
idempotency str.id(16)
A random key changes on every retry, so Frontbacked cannot tell that two requests are the same action.
Endpoint Writes
Endpoint process blocks can use controlled write helpers when the endpoint should create, edit, or delete related posts.
endpoint.post leadWebhook("/lead-webhook") {
verify $request.body.email
idempotency `lead:${str.lower(str.trim($request.body.email))}:${$request.body.reference}`
process {
$context.email = str.lower(str.trim($request.body.email));
trigger.post.create("leads", {
email: $context.email,
leadId: "$id",
externalReference: $request.body.reference,
source: $request.body.source || "endpoint",
createdAt: time.now()
});
return {
ok: true,
email: $context.email
};
}
}
Pair endpoint-created records with idempotency when repeats would be harmful. Frontbacked generates the related post id, and the exact string "$id" can be used inside the related post data when that generated id should be copied into a normal field.
Practical Patterns
| Pattern | Why endpoints help |
|---|---|
| Availability check | Return { ok, available } without storing a new post. |
| Quote calculator | Compute from $private, $currency, request body, and current user state. |
| Action button | Let one click perform a guarded server-side operation. |
| Callback-style workflow | Accept a structured payload, verify it, and update related data. |
| Paid action | Use chargeUser(...) inside a guarded process when the action should require wallet confirmation. |
Keep endpoints small and explicit. If the result is a normal user-created record, prefer uploadPost. If the action needs a custom request shape, a custom response, idempotency, or controlled side effects, use an FRL endpoint.