API Layer
Build on the @repo/api Hono backend with typed RPC, public /v1 routes, and user API keys.
The backend is one Hono app in @repo/api, exporting a configured app plus the AppType type that powers end-to-end RPC. Everything mounts under apiBasePath (the pathname of your API URL): internal routes back the Next.js frontend, public /v1 routes serve external integrations.
Route tree
packages/api/src/index.ts composes internal, public, and Inngest sub-routers into one routes object.
| Group | Mount | Access | Source |
|---|---|---|---|
| Health | /v1/health | Public | routes/public/v1/health.ts |
| IP | /v1/ip | Public | routes/public/v1/ip.ts |
| Content | /v1/content | Public when config.contentApi.enabled | routes/public/v1/content.ts |
| OpenAPI spec | /openapi.json, /llms.txt | Public | routes/public/index.ts |
| Auth, billing, dashboard | /auth, /billing, /dashboard | Internal | routes/internal/ |
| Admin, API keys, audit | /admin, /api-keys, /audit | Internal | routes/internal/ |
| Notifications, storage, geo | /notifications, /storage, /geo | Internal | routes/internal/ |
| Background jobs | /inngest | Inngest-signed | routes/inngest.ts |
/v1/healthreturns{ status: "success", version }with an ETag; deploy smoke tests readversion./v1/contentis filesystem-backed CRUD over markdown (Node.js runtimes only), gated byconfig.contentApi.enabledplus anX-Content-Api-Keyheader matchingCONTENT_API_KEY; when off the route is never mounted.- CORS is driven by
config.originswithcredentials: true. A global limiter caps external traffic at 200 requests / 60s; internal requests skip it viaisInternalRequest.
Typed RPC and validation
AppType captures every route, but only under two rules.
- Chain handlers: keep each router on one fluent
new Hono().get(...).post(...)chain. Reassigningappbetween calls drops routes from inference. - Validate with
sValidatorfrom@hono/standard-validatoragainst Zod schemas; read parsed input withc.req.valid("json" | "query" | "param").
// packages/api/src/index.ts
const routes = new Hono()
.route("/", internalRoutes)
.route("/", publicRoutes)
.route("/", inngestRoutes);
app.basePath(apiBasePath).route("/", routes);
export type AppType = typeof routes;Scalar API docs
When config.apiDocs is true (default), an interactive Scalar reference mounts at {apiBasePath}/docs - /api/docs, not /api/v1/docs - reading {apiBasePath}/openapi.json. Set it false to remove the route.
config.performanceMonitor.enabled toggles the Hono request logger (middleware/performance.ts, logs METHOD /path STATUS Nms cached). Fullstack projects generate with it false; separate-backend projects default to true. See Performance monitoring.
The Next.js clients
Build the typed client with hc<AppType> from hono/client - never import the backend into app code. The right client depends on where you call it.
| Client | File | Use in | Behavior |
|---|---|---|---|
api | apps/web-next/lib/api/client.ts | Client Components | "use client"; credentials: "include" so the browser attaches the session cookie |
getServerApi() | apps/web-next/lib/api/server.ts | Server Components / Actions | "server-only"; forwards the request's cookie header via next/headers |
// client component
import { api } from "@/lib/api/client";
const res = await api.v1.health.$get();
// server component / action
import { getServerApi } from "@/lib/api/server";
const client = await getServerApi();See Data fetching for the full SSR cookie-forwarding pattern.
User API keys
User-facing keys (the Better Auth api-key plugin) are gated by config.apiKeys.enabled and served from routes/internal/api-keys.ts behind authGuard. Each key bakes in a per-plan rate limit at creation, resolved by resolveApiKeyRateLimitPlan (@repo/payments) from pricingConfig. Plugin config, routes, and quotas: API keys.
Frequently asked questions
Why doesn't my new route appear in the client's types?
The route chain was broken. Keep every .route()/.get() on the single fluent chain that feeds export type AppType = typeof routes - a detached call is invisible to inference.
Why are the docs at /api/docs and not under /v1?
Scalar mounts at the base path ({apiBasePath}/docs), not the versioned namespace. Toggle it with config.apiDocs.
Which client do I import in a Server Component?
Use getServerApi() from lib/api/server.ts - it is server-only and forwards cookies. Use api only in Client Components.
Can an external client call internal routes with an API key?
A valid key authenticates any route that accepts it, but /v1/* is the documented public surface. Treat non-/v1 groups as first-party.