End-to-end Next.js project setup for 2026.
Bootstrap
npx create-next-app@latest myapp \
--typescript --tailwind --app --src-dir --eslint --import-alias "@/*"
cd myapp
Dependencies
npm i next-auth@beta @auth/prisma-adapter @prisma/client \
@tanstack/react-query zod react-hook-form @hookform/resolvers \
next-intl next-themes \
zustand sonner clsx tailwind-merge class-variance-authority
npm i -D prisma \
vitest @vitest/coverage-v8 jsdom \
@testing-library/react @testing-library/jest-dom @testing-library/user-event \
prettier prettier-plugin-tailwindcss \
@tanstack/react-query-devtools
Layout
src/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── globals.css
│ ├── providers.tsx
│ ├── (auth)/login/page.tsx
│ ├── (app)/
│ │ ├── layout.tsx
│ │ ├── dashboard/page.tsx
│ │ └── ...
│ └── api/...
├── components/
│ └── ui/
├── lib/
│ ├── db.ts
│ ├── env.ts
│ ├── auth.ts
│ └── cn.ts
├── stores/
└── types/
prisma/schema.prisma
lib/env.ts
import { z } from "zod";
const Env = z.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
AUTH_GITHUB_ID: z.string(),
AUTH_GITHUB_SECRET: z.string(),
NEXT_PUBLIC_APP_URL: z.string().url(),
});
export const env = Env.parse(process.env);
lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { db?: PrismaClient };
export const db = globalForPrisma.db ?? new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.db = db;
lib/cn.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SessionProvider } from "next-auth/react";
import { ThemeProvider } from "next-themes";
import { Toaster } from "sonner";
import { useState } from "react";
export function Providers({ children }: { children: React.ReactNode }) {
const [qc] = useState(() => new QueryClient({
defaultOptions: { queries: { staleTime: 60_000 } },
}));
return (
<SessionProvider>
<QueryClientProvider client={qc}>
<ThemeProvider attribute="class" defaultTheme="system">
{children}
<Toaster />
</ThemeProvider>
</QueryClientProvider>
</SessionProvider>
);
}
app/layout.tsx
import "./globals.css";
import { Inter } from "next/font/google";
import { Providers } from "./providers";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
export const metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL!),
title: { default: "MyApp", template: "%s — MyApp" },
description: "...",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning className={inter.variable}>
<body className="font-sans">
<Providers>{children}</Providers>
</body>
</html>
);
}
prisma/schema.prisma
generator client { provider = "prisma-client-js" }
datasource db { provider = "postgresql", url = env("DATABASE_URL") }
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
body String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
enum Role { USER ADMIN }
npx prisma migrate dev --name init
npx prisma generate
next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
output: "standalone",
experimental: { reactCompiler: true },
images: {
remotePatterns: [{ protocol: "https", hostname: "**.example.com" }],
},
async headers() {
return [{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
}];
},
};
scripts
{
"scripts": {
"dev": "next dev",
"build": "prisma generate && next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run --coverage",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
}
}
vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./test/setup.ts"],
},
});
.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: "20", cache: "npm" }
- run: npm ci
- run: npx prisma generate
- run: npm run typecheck
- run: npm run lint
- run: npm run test:run
- run: npm run build
Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json prisma ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate && npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
.gitignore
node_modules/
.next/
out/
.env.local
.env.production.local
coverage/
*.log
Conventions
- Server components by default;
"use client"only when needed. - Push client boundary to the leaves.
- Validate all input at server boundary with Zod.
- One QueryClient per request on server.
- Standalone output for Docker, Vercel for managed.
Read this next
That’s 20 Next.js cheatsheets. Next category: Django.
If you want my full Next.js starter (Auth + Prisma + Tailwind + Tests + Docker), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .