Better-TS-Stack
Modules

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 types

The 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

TechnologyVersionPurpose
Next.js16.3.4React framework with App Router
React19.2.8UI library
TypeScript6.0Type safety
Tailwind CSS4.xUtility-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   # eslint

Integration 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 adapter
  • lib/auth-client.ts - createAuthClient() for use in client components
  • lib/auth-schema.ts - shared Zod validation and auth error formatting
  • app/api/auth/[...all]/route.ts - the Better Auth route handler
  • app/sign-in/page.tsx - login route with server-side redirect when signed in
  • app/sign-up/page.tsx - signup route with server-side redirect when signed in
  • app/dashboard/page.tsx - protected route example with server-side session check
  • components/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:3000

With 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 config

Key conventions

  • ~/* path alias mapped to ./src/* (TanStack-native, resolved by tsconfigPaths: true in the Vite config). Use ~/components/... and ~/lib/... in imports.
  • File-based routing lives in src/routes/. TanStack Router regenerates src/routeTree.gen.ts during Vite development and builds.
  • Nitro for production: as in TanStack's hosting guide, vite build runs the Nitro Vite plugin, which writes a self-contained Node.js server to .output/. The start script runs it with node and loads .env first. vite preview serves that build. vite dev uses 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 src
  • PORT in .env sets the production server port (default 3000). vite dev uses the port in vite.config.ts.
  • The generated package.json sets "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 lint

Integration with Auth Module

If Better Auth is selected (requires a database), the auth module additionally generates:

  • src/lib/auth.ts - betterAuth() with tanstackStartCookies() and the matching adapter
  • src/lib/auth-client.ts - createAuthClient() for client components
  • src/lib/auth-functions.ts - getSession() server function built on createServerFn + auth.api.getSession
  • src/routes/api/auth/$.ts - the Better Auth route handler via auth.handler(request)
  • src/routes/sign-in.tsx / src/routes/sign-up.tsx - login/signup routes with beforeLoad redirect when signed in
  • src/routes/dashboard.tsx - protected route example using redirect({ to: "/sign-in" }) in beforeLoad
  • src/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.

On this page