Full-Stack Frameworks

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:

next.config.ts
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:

tsconfig.json
{
  "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:

src/server/context.ts
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:

src/server/taser.ts
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:

src/app/api/[[...slug]]/route.ts
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/:

src/server/routes/users.get.ts
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:

src/client.ts
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: ... }) singleton

Data Fetching & Mutation Patterns

1. React Server Components (RSC)

Consume your Taser endpoints directly on the server inside React Server Components using your typed client:

src/app/page.tsx
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:

src/app/actions/create-user.ts
"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:

src/app/components/user-list.tsx
"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:

ResponsibilityNext.js App Router (RSC & Pages)Taser Subsystem (src/server/)
Primary FocusServer-rendered UI, HTML streaming, metadata, layout compositionStructured REST API, JSON endpoints, binary streams, webhooks
Execution ContextReact Server Components & Client Components (src/app/**/*.tsx)Type-safe route handlers (src/server/routes/**/*.ts)
ConsumersWeb browser page visitsMobile apps, SPA client hooks, webhooks, external developer APIs
State & MiddlewareNext.js Edge middleware (middleware.ts)Directory layout middleware pipelines (src/server/routes/$.ts)
ValidationManual or form-action validationStandard Schema validation (Zod, ArkType, Valibot)
Response SafetyReact component propsCompile-time .returns() contracts & typed success responses

Next Steps