Frontend Module
Next.js 16 or TanStack Start with React 19 and Tailwind CSS v4
Overview
The Frontend module generates a full-stack React application. When you choose the Full-stack application type, the CLI asks you to pick Next.js 16 (App Router) or TanStack Start 1.0. Both use React 19 and Tailwind CSS v4.
The frontend module produces a single project at the project root. There
are no separate frontend/ or backend/ subdirectories.
What You Get
app/
|-- layout.tsx # Root layout with fonts and metadata
|-- page.tsx # Home page
\-- globals.css # Tailwind CSS v4 import + base styles
components/ui/ # shadcn-compatible base UI primitives
lib/utils.ts # cn() helper for class merging
components.json # shadcn CLI-compatible component config
public/ # Static assets (Next.js defaults)
next.config.ts # Next.js configuration
postcss.config.mjs # PostCSS setup for Tailwind
tsconfig.json # TypeScript configuration
eslint.config.mjs # ESLint configuration
next-env.d.ts # Next.js TypeScript environment typesThe Next.js template includes a few shadcn-compatible UI components (button,
card, field, input). The auth pages use them, and you can add more with
the shadcn CLI.
Technology Stack
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16.3.4 | React framework with App Router |
| React | 19.2.8 | UI library |
| TypeScript | 6.0 | Type safety |
| Tailwind CSS | 4.x | Utility-first CSS |
Generated Files
app/layout.tsx
Root layout with Geist fonts and base metadata:
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}app/globals.css
Tailwind CSS v4 setup with system dark-mode support:
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}Commands
npm run dev # next dev
npm run build # next build
npm run start # next start
npm run lint # eslintIntegration with Auth Module
If Better Auth is selected (requires a database), the auth module adds:
lib/auth.ts- server-side Better Auth configuration with the matching adapterlib/auth-client.ts-createAuthClient()for use in client componentslib/auth-schema.ts- shared Zod validation and auth error formattingapp/api/auth/[...all]/route.ts- the Better Auth route handlerapp/sign-in/page.tsx- login route with server-side redirect when signed inapp/sign-up/page.tsx- signup route with server-side redirect when signed inapp/dashboard/page.tsx- protected route example with server-side session checkcomponents/auth/*- login, signup, and sign-out UI components
// lib/auth.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
import { prismaAdapter } from "better-auth/adapters/prisma";
import client from "./prisma";
export const auth = betterAuth({
database: prismaAdapter(client, { provider: "postgresql" }),
emailAndPassword: { enabled: true },
plugins: [nextCookies()],
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});Auth route handler
// app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);Client-side auth
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();The generated sign-in and sign-up forms use React Hook Form + Zod on the client
and call authClient.signIn.email(...) / authClient.signUp.email(...).
proxy.ts redirects visitors without a session from /dashboard to /sign-in.
BETTER_AUTH_SECRET and BETTER_AUTH_URL are server-side variables. Do not
prefix them with NEXT_PUBLIC_.
Environment Variables
The base Next.js project needs no variables. Next.js picks its port before it loads .env, so set it in the shell instead, as in PORT=3001 npm run dev.
NODE_ENV is not written to .env. The dev, build, and start commands set it, so npm start runs in production mode.
With Better Auth added:
BETTER_AUTH_SECRET=please-change-me-to-a-random-string
BETTER_AUTH_URL=http://localhost:3000With a database added, the relevant DATABASE_URL or MONGODB_URI is also included.
TanStack Start
TanStack Start uses the Vite dev server, TanStack Router for type-safe file-based routing, and React Query with SSR.
src/
|-- routes/ # File-based routes (__root.tsx, index.tsx)
|-- routeTree.gen.ts # Generated route tree
|-- router.tsx # createRouter() + QueryClient + SSR query integration
|-- lib/utils.ts # cn() helper for class merging
|-- components/ui/ # shadcn-compatible base UI primitives
|-- styles/app.css # Tailwind CSS v4 import + base styles
vite.config.ts # tanstackStart() + tailwind + react plugins
components.json # shadcn CLI-compatible component configKey conventions
~/*path alias mapped to./src/*(TanStack-native, resolved bytsconfigPaths: truein the Vite config). Use~/components/...and~/lib/...in imports.- File-based routing lives in
src/routes/. TanStack Router regeneratessrc/routeTree.gen.tsduring Vite development and builds. - Nitro for production: as in TanStack's hosting guide,
vite buildruns the Nitro Vite plugin, which writes a self-contained Node.js server to.output/. Thestartscript runs it withnodeand loads.envfirst.vite previewserves that build.vite devuses TanStack Start's own dev server, without Nitro:
npm run build # vite build (writes .output/)
npm run start # node --env-file-if-exists=.env .output/server/index.mjs
npm run dev # vite dev
npm run type:check # tsc --noEmit
npm run lint # eslint srcPORTin.envsets the production server port (default 3000).vite devuses the port invite.config.ts.- The generated
package.jsonsets"type": "module"so the ESM-only TanStack Vite plugin loads correctly.
Commands
bun run dev # vite dev
bun run build # vite build
bun run start # node --env-file-if-exists=.env .output/server/index.mjs
bun run type:check
bun run lintIntegration with Auth Module
If Better Auth is selected (requires a database), the auth module additionally generates:
src/lib/auth.ts-betterAuth()withtanstackStartCookies()and the matching adaptersrc/lib/auth-client.ts-createAuthClient()for client componentssrc/lib/auth-functions.ts-getSession()server function built oncreateServerFn+auth.api.getSessionsrc/routes/api/auth/$.ts- the Better Auth route handler viaauth.handler(request)src/routes/sign-in.tsx/src/routes/sign-up.tsx- login/signup routes withbeforeLoadredirect when signed insrc/routes/dashboard.tsx- protected route example usingredirect({ to: "/sign-in" })inbeforeLoadsrc/components/auth/*- login, signup, and sign-out UI components
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
plugins: [tanstackStartCookies()],
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});setupRouterSsrQueryIntegration connects TanStack Query to router SSR.
Authentication is checked separately by getSession() in each route's
beforeLoad, and protected routes expose the returned user through route
context.
Extending Next.js
Add a page
// app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>About</h1>
</main>
);
}Add a server-side API route
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: "Hello from Next.js!" });
}Add a component
// components/Header.tsx
export function Header() {
return (
<header className="border-b p-4">
<nav>Navigation here</nav>
</header>
);
}Components that use useState, useEffect, or event handlers must have "use client" at the top of the file. All other components default to Server
Components.