Next.js App Router
Build fullstack Next.js 15+ App Router applications with a dedicated Taser REST API subsystem. Type-safe data fetching in React Server Components, Server Actions, and Client Components.
Next.js App Router provides a React framework for UI rendering, server components, and streaming HTML. By integrating @taserjs/router-plugin/next, you can embed a structured, file-based REST API subsystem with cascading directory middleware, Standard Schema validation, and compile-time return contracts under /api.
Why Pair Taser with Next.js?
Clean Architectural Boundaries
Keep React UI components in `src/app/` and structured, type-safe REST API endpoints in `src/server/`.
Cascading Directory Middleware
Apply scoped auth, logging, and rate limiting with folder-level middleware (`$.ts`) instead of monolithic edge middleware.
Compile-Time Return Contracts
Enforce compile-time return shapes with `.returns()`. Eliminate response drift across server and client.
Universal Client SDK
Call endpoints in React Server Components, Server Actions, and client hooks with 100% type inference.
Installation
Install the necessary dependencies in your Next.js project:
pnpm add @taserjs/router @taserjs/router-client zod
pnpm add -D @taserjs/router-plugin
Integration Steps
1. Wrap next.config.ts
Use createTaser (or withTaser) from @taserjs/router-plugin/next to wrap your Next.js configuration:
import type { NextConfig } from "next";
import { createTaser } from "@taserjs/router-plugin/next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
const withTaser = createTaser({
serverDir: "src/server", // Directory hosting Taser server code & routes
entry: "@/server/taser", // Path alias to your router instance
basePath: "/api", // URL prefix where Taser REST endpoints dispatch
});
export default withTaser(nextConfig);2. Configure TypeScript Paths (tsconfig.json)
Configure path aliases for @/* and @/.taser/* so Next.js and your IDE resolve route files, client singletons, and generated disk artifacts:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@/.taser/*": ["./.taser/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".taser/**/*.ts",
".taser/**/*.d.ts"
]
}3. Create Router Instance & Context
Create src/server/context.ts to manage application boot and request-scoped state:
import { createContext } from "@taserjs/router";
export const context = createContext({
boot: () => ({
logger: console,
}),
request: () => ({
requestId: crypto.randomUUID(),
}),
});Create src/server/taser.ts to initialize your createTaserApp builder:
import { createTaserApp } from "@taserjs/router";
import { notFound } from "@taserjs/router/reply";
import { context } from "./context";
export default createTaserApp({
response: { validate: true },
})
.context(context)
.notFound(() => notFound({ message: "Not Found" }));4. Create Catch-All Route Handler
Create src/app/api/[[...slug]]/route.ts (or app/api/[[...slug]]/route.ts). This forwards incoming Next.js API requests directly to Taser's compiled entry module:
import { app } from "@/.taser/entry";
const handle = (request: Request) => app.fetch(request);
export const GET = handle;
export const POST = handle;
export const PUT = handle;
export const DELETE = handle;
export const PATCH = handle;
export const OPTIONS = handle;
export const HEAD = handle;5. Define File-Based REST Routes
Create your endpoints in src/server/routes/:
import { json } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
const GET = t
.get("/users")
.query(
z.object({
limit: z.coerce.number().default(10),
}),
)
.returns({
200: z.object({
users: z.array(
z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
),
total: z.number(),
}),
});
export type RouteContext = typeof GET.$Infer.Context;
export default GET.handler((ctx) => {
return json({
users: [
{ id: "usr_1", name: "Alice", email: "alice@example.com" },
{ id: "usr_2", name: "Bob", email: "bob@example.com" },
],
total: 2,
});
});6. Create Typed Client Singleton
Create src/client.ts to export a pre-configured @taserjs/router-client instance:
import { createClient } from "@taserjs/router-client";
import type { app } from "@/.taser/entry";
export const api = createClient<typeof app>({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api",
});Project Structure Overview
Here is the recommended file layout for a fullstack Next.js App Router project with Taser:
my-next-app/
├── next.config.ts # withTaser(nextConfig) with serverDir, entry, basePath
├── tsconfig.json # paths: @/*, @/.taser/* and .taser include
├── package.json
├── .taser/ # Auto-generated disk artifacts & ambient types
│ ├── entry.ts # Exports compiled `app` with routes
│ ├── manifest.ts # Static route manifest tree
│ ├── app.ts # Composed router module
│ └── types/ # Generated TypeScript ambient route declarations
└── src/
├── app/ # Next.js App Router (React UI & Pages)
│ ├── layout.tsx # Root HTML shell
│ ├── page.tsx # RSC page fetching from Taser API
│ └── api/
│ └── [[...slug]]/
│ └── route.ts # Catch-all route handler forwarding to app.fetch()
├── server/ # Taser REST API Subsystem
│ ├── taser.ts # createTaserApp instance definition
│ ├── context.ts # Boot and request context
│ └── routes/ # File-based REST endpoints
│ ├── $.ts # Root layout middleware (optional)
│ └── users.get.ts # GET /api/users endpoint
└── client.ts # createClient({ baseUrl: ... }) singletonData Fetching & Mutation Patterns
1. React Server Components (RSC)
Consume your Taser endpoints directly on the server inside React Server Components using your typed client:
import { api } from "@/client";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const res = await api.users.$get({
query: { limit: 20 },
});
if (res.status !== 200) {
return <div>Failed to load users.</div>;
}
const { users } = await res.json();
return (
<main className="p-8">
<h1 className="text-2xl font-bold">User Directory</h1>
<ul className="mt-4 space-y-2">
{users.map((user) => (
<li key={user.id} className="p-3 border rounded">
<span className="font-semibold">{user.name}</span> ({user.email})
</li>
))}
</ul>
</main>
);
}2. Next.js Server Actions
Trigger mutations inside Server Actions and revalidate page paths with complete return shape safety:
"use server";
import { revalidatePath } from "next/cache";
import { api } from "@/client";
export async function createUserAction(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const res = await api.users.$post({
body: { name, email },
});
if (res.status === 201) {
revalidatePath("/");
return { success: true };
}
return { success: false, error: "Failed to create user" };
}3. Client Components & React Hooks
Use Taser client calls with TanStack Query or SWR inside Client Components:
"use client";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/client";
export function UserList() {
const { data, isLoading } = useQuery({
queryKey: ["users"],
queryFn: async () => {
const res = await api.users.$get({ query: { limit: 10 } });
if (res.status === 200) {
return await res.json();
}
throw new Error("Failed to fetch users");
},
});
if (isLoading) return <div>Loading users...</div>;
return (
<div>
{data?.users.map((u) => (
<div key={u.id}>{u.name}</div>
))}
</div>
);
}Architecture: React Server Components (RSC) vs Taser API
When pairing Taser with Next.js App Router, understanding the division of responsibilities ensures clean system design:
| Responsibility | Next.js App Router (RSC & Pages) | Taser Subsystem (src/server/) |
|---|---|---|
| Primary Focus | Server-rendered UI, HTML streaming, metadata, layout composition | Structured REST API, JSON endpoints, binary streams, webhooks |
| Execution Context | React Server Components & Client Components (src/app/**/*.tsx) | Type-safe route handlers (src/server/routes/**/*.ts) |
| Consumers | Web browser page visits | Mobile apps, SPA client hooks, webhooks, external developer APIs |
| State & Middleware | Next.js Edge middleware (middleware.ts) | Directory layout middleware pipelines (src/server/routes/$.ts) |
| Validation | Manual or form-action validation | Standard Schema validation (Zod, ArkType, Valibot) |
| Response Safety | React component props | Compile-time .returns() contracts & typed success responses |
Next Steps
TanStack Start
Build fullstack React applications with TanStack Start and Taser. Type-safe data fetching in TanStack Router loaders, React Query, and multi-runtime Nitro deployment.
Vite Plugin
Integrate Taser with Vite using @taserjs/router-plugin/vite. Virtual route resolution, ambient type generation, instant HMR, and standalone builds.