CHART/DOCS/API ROUTES

API routes & secrets

The server half of the framework: routes, env, sessions, middleware.

Pages render HTML; API routes answer requests. Both live in the same project, share the same env() secrets, and can be gated by one middleware. The server primitives are deliberately small: standard Web Request/Response objects, an HMAC-signed cookie, and three middleware result shapes.

API routes

A top-level api/ directory maps to /api/* routes, exactly like pages/ maps to pages. Every file must begin with // @runsOn api. A file exports one handler per HTTP method - the supported-methods list is the export list:

// src/api/checkout.ts - maps to /api/checkout
// @runsOn api
export function POST(req: Request): Promise<Response> {
  return (async () => {
    const body = await req.json();
    const s = session();
    const visits = (s ? s.visits : 0) + 1;
    return setSession(
      { visits },
      new Response(JSON.stringify({ ok: true, visits }), { status: 201 }),
    );
  })();
}

export function GET(req: Request) {
  const s = session();
  return new Response(JSON.stringify({ visits: s ? s.visits : null }), {
    headers: { 'Content-Type': 'application/json' },
  });
}
  • Handlers receive the standard Web Request and return a Response (or a promise of one).
  • env() is available; data() is not - it is page-render-time fetching (API_DATA_CALL).
  • API files are not components: no props, no JSX, no signal rules - ordinary TypeScript.
  • marisjs dev and @marisjs/adapter-node serve them; @marisjs/adapter-static refuses builds that contain them.

Secrets: env()

env("STRIPE_SECRET_KEY") reads a value from a .env file at build time. The values are baked into the compiled server modules as a snapshot - env() never touches process.env at runtime. A missing key yields undefined, so the fallback idiom works: env("PORT") ?? "3000".

  • Callable only from @runsOn server, @runsOn api, and middleware.ts. In a client file it is a hard error (CLIENT_ENV_ACCESS).
  • marisjs init writes a .gitignore that excludes .env and a .env.example with no real values.
  • Real process environment takes precedence over the .env file - CI can set real secrets without editing it.
  • An env() result passed into a client:hydrate prop triggers a best-effort lint warning (ENV_LEAK_TO_CLIENT_PROP) - secrets do not belong in client bundles.

Sessions

session() reads and HMAC-verifies a signed cookie; setSession(data, response) signs the data, attaches it as a Set-Cookie header, and returns the response. Verification runs on every read, and every failure mode - absent, malformed, tampered - fails safe to null.

  • The cookie is HttpOnly, SameSite=Lax, and Secure when built with NODE_ENV=production.
  • The signing secret comes from env('SESSION_SECRET') and must be at least 16 characters - a module that uses sessions fails the build without one.
  • Sessions are stateless by design: no server-side store, no per-session revocation. Rotating the secret invalidates everything.

Middleware

One request gate for the whole site: middleware.ts at the project root, exporting middleware and matcher. It runs before every matching request - pages, API routes, and static files alike:

// middleware.ts - project root, sibling to pages/ and api/
export function middleware(req: Request) {
  const s = session();
  if (!s) return redirect('/login');
  return next();
}

export const matcher: string[] = ['/admin/*'];
  • The function must return exactly one of next(), redirect(url, status?), or respond(response) - anything else is a hard error (MIDDLEWARE_RESULT).
  • matcher is an array of path patterns. Matching is case-sensitive, trailing slashes are significant, and * matches any run of characters.
  • session() and env() are callable here; data() is not.
  • Middleware must not consume the request body unless it respond()s - the matched route receives the request fresh.
  • @marisjs/adapter-node runs it; @marisjs/adapter-static refuses any build that declares middleware.
Where secrets live: server modules are compiled under the private dist/_server/ tree and are never served statically. Client bundles carry no secrets by construction - the validator blocks env() and session() in @runsOn client files outright.