Guides

HTTP endpoints

A guide to adding HTTP APIs alongside your application pages.

Custom HTTP endpoints let an Effront application return JSON alongside its rendered Pages, using the same application services. The example adds GET /api/greeting to the application from the services guide and applies a shared response header to the Page and API.

Define a JSON endpoint

Create src/http.ts:

import { Effect } from "effect";
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
import { Greeting } from "./greeting";

export const GreetingApi = HttpRouter.use(
  Effect.fn(function* (router) {
    const greeting = yield* Greeting;
    yield* router.add(
      "GET",
      "/api/greeting",
      Effect.map(greeting.message("Ada"), (message) => HttpServerResponse.jsonUnsafe({ message })),
    );
  }),
);

Choose a path that does not overlap Page or Server Function URLs, and leave /_effront reserved.

Register the route and its service

In src/entry.effront.tsx, add Layer to the effect import and import GreetingApi.

// src/entry.effront.tsx: replace the effect import.
import { Effect, Layer } from "effect";
// Add to the imports.
import { GreetingApi } from "./http";

// Replace the default export.
const ApplicationLayer = GreetingApi.pipe(Layer.provideMerge(Greeting.layer));

export default EFFRONT.make({ routes, layer: ApplicationLayer });

Layer.provideMerge supplies Greeting to the route registration and retains it for the Page. Open /api/greeting. Expect status 200, content type application/json, and:

{ "message": "Hello, Ada." }

The existing / Page still displays the greeting.

Keep resources request-local

The application Layer is built for each request, including custom HTTP requests. Keep connections and other scoped resources within that request, whose scope lasts through response-body completion, failure, or cancellation.

Add a shared response header

Create src/application-layer.ts:

import { Effect, Layer } from "effect";
import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
import { Greeting } from "./greeting";
import { GreetingApi } from "./http";

const GlobalHeaders = HttpRouter.middleware(
  (httpEffect) =>
    Effect.map(httpEffect, HttpServerResponse.setHeader("x-content-type-options", "nosniff")),
  { global: true },
);

export const ApplicationLayer = Layer.mergeAll(GreetingApi, GlobalHeaders).pipe(
  Layer.provideMerge(Greeting.layer),
);

In src/entry.effront.tsx, import the shared Layer and remove the local definition and unused imports:

// src/entry.effront.tsx: replace the effect import to remove Layer.
import { Effect } from "effect";
// Replace the GreetingApi import.
import { ApplicationLayer } from "./application-layer";

// Remove the local ApplicationLayer declaration.
export default EFFRONT.make({ routes, layer: ApplicationLayer });

Both / and /api/greeting now include x-content-type-options: nosniff.

global: true covers Pages, Server Functions, custom routes, and unmatched requests. Use scoped Middleware for a policy limited to one Routes group.