Loading content…
Loading content…
Build complete applications using Next.js as a full-stack framework
| Aspect | React + Node | Next.js Full-Stack |
|---|---|---|
| Deployment | 2 codebases, 2 deploys | Single deployment |
| API | JSON over HTTP | Direct function calls |
| Type Safety | Partial | Full stack (TypeScript) |
| Development | Complex setup | Single npm run dev |
| Performance | Network latency | Server components, no latency |
Next.js Application
├── Frontend (React components)
├── API Routes (Backend)
└── Database (via API/ORM)
// app/users/page.tsx
// This runs only on the server
async function getUsers() {
const users = await db.user.findMany();
return users;
}
export default async function UsersPage() {
const users = await getUsers();
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
// app/api/users/route.ts
export async function GET(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const { name, email } = await request.json();
const user = await db.user.create({ name, email });
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
// In Next.js 15+, dynamic parameters are Promises and must be awaited!
const { id } = await params;
const user = await db.user.findUnique({
where: { id }
});
if (!user) {
return NextResponse.json(
{ error: "Not found" },
{ status: 404 }
);
}
return NextResponse.json(user);
}
Senior Developer Wisdom
params in Page components and API route handlers is a Promise and must be resolved using await params or React's use hook before accessing its properties.// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
// schema.prisma
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
user User @relation(fields: [userId], references: [id])
userId Int
}
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Validate
if (!title || !content) {
throw new Error("Missing fields");
}
// Create in database
const post = await db.post.create({
data: { title, content, userId: 1 }
});
// Revalidate
revalidatePath("/posts");
return post;
}
// app/posts/new/page.tsx
import { createPost } from "@/app/actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create</button>
</form>
);
}
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { verifyToken } from "./lib/auth";
export async function middleware(request: NextRequest) {
const token = request.cookies.get("auth")?.value;
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
if (!token || !verifyToken(token)) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"]
};
"use client";
import { useEffect, useState } from "react";
interface Post {
id: number;
title: string;
}
export function LivePosts() {
// Use generic types in useState so TS knows posts is an array of Post.
// Otherwise, TS infers posts to be type never[], causing build errors on mapping.
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
const interval = setInterval(async () => {
try {
const res = await fetch("/api/posts");
if (res.ok) {
const data = await res.json();
setPosts(data);
}
} catch (err) {
console.error("Error polling posts:", err);
}
}, 5000); // Poll every 5 seconds
return () => clearInterval(interval);
}, []);
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
.env files to separate configuration from code. This keeps secrets out of your repository and lets you change settings per environment without code changes.# .env.local
DATABASE_URL=postgres://user:pass@localhost:5432/app
API_KEY=your-secret-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Pro Tip
NEXT_PUBLIC_ are exposed to the browser. Everything else stays server-only.# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "run", "start"]
.env files (not committed) and pass them at runtime:docker run --env-file .env.local -p 3000:3000 your-app-image
Pro Tip
.env files for each environment and keep them out of source control.✓ Database migrations
✓ Environment variables
✓ Error handling
✓ Authentication/Authorization
✓ Input validation
✓ Rate limiting
✓ Logging
✓ Monitoring
✓ Backup strategy
Common Pitfall
Pro Tip
Next.js Full-Stack Development
Marking it complete updates your roadmap progress percentage.