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 };
  }
}
PartPurpose
methodOne of get, post, put, or delete.
endpointNameA unique name for this endpoint in the rule file.
"/path/:param"Public path under /fb-endpoints. Params can use :id or {id}.
verifyOptional guard evaluated before process. Return truthy to continue.
idempotencyOptional stable key that makes repeated matching requests safe.
processEndpoint 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.

ValueExample
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:

SituationResult
First request for a keyThe endpoint runs normally.
Same key, same request, already completedFrontbacked returns the saved response.
Same key, same request, still runningFrontbacked returns a processing response.
Same key, different request body/query/pathFrontbacked 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

PatternWhy endpoints help
Availability checkReturn { ok, available } without storing a new post.
Quote calculatorCompute from $private, $currency, request body, and current user state.
Action buttonLet one click perform a guarded server-side operation.
Callback-style workflowAccept a structured payload, verify it, and update related data.
Paid actionUse 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.