Full-Stack Frameworks

Next.js App Router

Build fullstack Next.js 15+ App Router applications with a dedicated Taser.js 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/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.js 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/client zod
pnpm add -D @taserjs/plugin

Integration Steps

1. Wrap next.config.ts

Use createTaser (or withTaser) from @taserjs/plugin/next to wrap your Next.js configuration:

next.config.ts
import type { NextConfig } from "next";
import { createTaser } from "@taserjs/plugin/next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
};

const withTaser = createTaser();

export default withTaser(nextConfig);

Put serverDir and related paths in taserjs.config.ts. Mount the API prefix with defineTaser().basePath("/api") in src/server/taser.ts.

2. Configure TypeScript Paths (tsconfig.json)

Configure @/* path aliases so Next.js and your IDE resolve src/ (including src/server/.taserjs/):

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "src/**/*", "**/*.ts", "**/*.tsx"]
}

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 with defineTaser() (uninstantiated definition; the compiled app lives in src/server/.taserjs/routes.gen.ts):

src/server/taser.ts
import { defineTaser } from "@taserjs/router";
import { notFound } from "@taserjs/router/reply";
import { context } from "./context";

export default defineTaser({
  response: { validate: true },
})
  .basePath("/api")
  .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.js's compiled entry module:

src/app/api/[[...slug]]/route.ts
import { app } from "@/server/.taserjs/routes.gen";

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 = Parameters<typeof GET.handler>[0];
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/client instance:

src/client.ts
import { createClient } from "@taserjs/client";
import type { AppManifest } from "@/server/.taserjs/routes.gen";

export const api = createClient<AppManifest>({
  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.js:

my-next-app/
├── next.config.ts                  # createTaser() / withTaser(nextConfig)
├── taserjs.config.ts               # serverDir, routesDir, outputDir, app
├── tsconfig.json                   # paths: @/* covering src/** including src/server/.taserjs
├── package.json
└── src/
    ├── app/                        # Next.js App Router (React UI & Pages)
    │   ├── layout.tsx              # Root HTML shell
    │   ├── page.tsx                # RSC page fetching from Taser.js API
    │   └── api/
    │       └── [[...slug]]/
    │           └── route.ts        # Catch-all: import { app } from "@/server/.taserjs/routes.gen"
    ├── server/                     # Taser.js REST API Subsystem
    │   ├── taser.ts                # defineTaser().basePath("/api") definition
    │   ├── context.ts              # Boot and request context
    │   ├── routes/                 # File-based REST endpoints
    │   │   ├── $.ts                # Root layout middleware (optional)
    │   │   └── users.get.ts        # GET /api/users endpoint
    │   └── .taserjs/
    │       └── routes.gen.ts       # Generated app + AppManifest + ambient types
    └── client.ts                   # createClient<AppManifest>({ baseUrl: ... })

Data Fetching & Mutation Patterns

1. React Server Components (RSC)

Consume your Taser.js 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.js 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.js API

When pairing Taser.js with Next.js App Router, understanding the division of responsibilities ensures clean system design:

ResponsibilityNext.js App Router (RSC & Pages)Taser.js 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