Skip to content

Next.js Setup

This guide explains how to integrate Toapi with the Next.js App Router.

Terminal window
npm install @toapi/server @toapi/client zod
# or
pnpm add @toapi/server @toapi/client zod

Create a central file to define your API structure. This acts as the source of truth for your routes.

src/api.ts
import { defineApi } from "@toapi/server";
export const api = defineApi();
// We'll add routes here later
// .route("/hello", import("./routes/hello"))

The Next.js App Router uses a catch-all route to handle dynamic API requests. Create a file at src/app/api/[...tapi]/route.ts.

This handler captures all requests to /api/* and routes them through Toapi.

src/app/api/[...tapi]/route.ts
import { createRequestHandler } from "@toapi/server";
import { api } from "@/api"; // Adjust import path as needed
const handler = createRequestHandler(api, {
basePath: "/api",
});
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const DELETE = handler;

To consume your API from client components, create a type-safe fetch client.

src/client.ts
"use client";
import { createFetchClient } from "@toapi/client";
import type { api } from "@/api";
// Assuming your API is mounted at /api
export const client = createFetchClient<typeof api.routes>("/api");

Create a route handler file. For example, src/routes/hello.ts:

src/routes/hello.ts
import { defineHandler, TResponse } from "@toapi/server";
export const GET = defineHandler({ authorize: () => true }, async () => {
return TResponse.json({ message: "Hello from Toapi!" });
});

And register it in src/api.ts:

src/api.ts
import { defineApi } from "@toapi/server";
export const api = defineApi().route("/hello", import("./routes/hello"));

Use the client in your interactive components:

src/app/page.tsx
"use client";
import { useState, useEffect } from "react";
import { client } from "@/client";
export default function ClientPage() {
const [msg, setMsg] = useState("");
useEffect(() => {
client.hello.get().then((data) => setMsg(data.message));
}, []);
if (!msg) return <div>Loading...</div>;
return <h1>{msg}</h1>;
}

For Server Components, skip the HTTP overhead by using createLocalClient. This calls your handlers directly.

Create a server-only client instance:

src/server-client.ts
import { createLocalClient } from "@toapi/server";
import { api } from "@/api";
export const serverClient = createLocalClient(api);

Then use it in your Server Component:

src/app/page.tsx
import { serverClient } from "@/server-client";
export default async function ServerPage() {
const data = await serverClient.hello.get();
return <h1>{data.message}</h1>;
}

To enable tag-based revalidation across all cache layers, expose the invalidation stream. defineApi automatically creates a PubSub instance, so no extra setup is needed for single-host deployments.

The catch-all handler already serves the stream at /api/__tapi/invalidations. If you prefer a dedicated route, add one at src/app/api/revalidate/route.ts:

src/app/api/revalidate/route.ts
import { streamRevalidatedTags } from "@toapi/server";
import { api } from "@/api";
export const GET = () => {
return streamRevalidatedTags({
cache: api.cache,
});
};

For setting up a service worker that connects to this endpoint, see the @toapi/worker package. For details on how the cache layers interact, see Caching Strategies.