Ludicord
Guide 13 · Full stack

API routes

Add trusted HTTP handlers with native Request/Response APIs and verified context.

Ludicord 3.1.0Public source 20c4889
Trust boundaryAuthentication resolves before handler code runs
Framework managed
POST/api/score

Browser request + session cookie

verified
route.tsctx.user + ctx.activity
trusted handler input

API routes keep credentials, authorization, database access, and other trusted work outside the Activity browser. Ludicord discovers method exports in app/api/**/route.ts and serves them from the matching /api path.

#Create a route

app/api/hello/route.tsts
1import type { LudicordRequest } from "ludicord/server";
2
3export function GET(request: LudicordRequest) {
4 const name = request.ludicord?.user.displayName ?? "player";
5 return Response.json({ message: `Hello, ${name}!` });
6}

Routes use the standard Web Request and Response APIs. A handler must return a Response or a promise resolving to one.

#Export HTTP methods

Supported exports are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. Ludicord supplies HEAD from GET and a basic OPTIONS response when you do not export them.

app/api/score/route.tsts
1export async function POST(request: LudicordRequest) {
2 const input = await request.json() as unknown;
3 const score = validateScore(input);
4 return Response.json({ score }, { status: 201 });
5}

Malformed JSON, bodies larger than the configured limit, unsupported methods, and route timeouts receive consistent runtime errors.

#Read session and verification context

LudicordRequest extends Request with three fields:

FieldMeaning
request.ludicordVerified Ludicord session or null for a public route
request.paramsDynamic route parameters
request.verificationDiscord proxy and Activity-instance verification status
tsts
1const session = requireLudicordSession(request);
2
3if (request.verification.activityInstance !== "verified") {
4 return Response.json({ error: "Verified Activity instance required" }, { status: 403 });
5}

Authentication proves who signed in. Authorization still belongs to the route: verify that the session user, guild, channel, role, or instance may perform the requested action.

#Dynamic routes

texttext
1app/api/players/[userId]/route.ts → /api/players/:userId
app/api/players/[userId]/route.tsts
1import type { LudicordRequest, RouteContext } from "ludicord/server";
2
3export function GET(request: LudicordRequest, context: RouteContext) {
4 return Response.json({ requested: context.params.userId });
5}

Route params are untrusted strings. Never use a browser-provided user or guild ID as proof of identity.

#Public routes

API modules require a valid session by default. Export public = true or auth = false only for endpoints that are intentionally anonymous.

app/api/health/route.tsts
1export const public = true;
2export function GET() {
3 return Response.json({ ok: true });
4}

Do not make a route public just to work around a broken OAuth or cookie setup.

#Handle cancellation

request.signal aborts when the client disconnects or the route timeout expires. Pass it to database or HTTP clients that support cancellation.

tsts
1const result = await fetch(upstreamUrl, { signal: request.signal });

#Call the route from React

tsxtsx
1const response = await fetch("/api/score", {
2 method: "POST",
3 headers: { "content-type": "application/json" },
4 body: JSON.stringify({ score }),
5});

Same-origin requests automatically include the Ludicord session cookie. Do not copy session data into request bodies.

#Next step

Use WebSockets when the server must push changes to connected Activities instead of waiting for another HTTP request.