LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 4420 min read

Next.js as a Full-Stack Framework

Understand exactly why Next.js is called a full-stack framework, and set up a complete backend inside one project — API, database, and auth included.

What "Full-Stack" Actually Means Here

A full-stack framework is one where both the frontend (what the user sees) and the backend (data, business logic, persistence) live in the same project, written in the same language, deployed together. Historically, a "React app" meant exactly the frontend half — you still needed a separate Node/Express (or Django, or Rails) project for the backend, its own repo, its own deploy pipeline, and a network boundary between the two. Next.js collapses that into one project.

Why Next.js Earns the Label

This isn't marketing — it comes down to three concrete capabilities you've already learned in this course, which together cover everything a backend is normally responsible for.

It Can Serve a Public API

Route Handlers (route.ts) respond to GET/POST/PUT/DELETE exactly like an Express or Fastify server would.

It Can Mutate Data

Server Actions run server-side logic directly from a form or button — no separate endpoint to wire up.

It Can Talk to a Database Directly

Server Components and Server Actions query Prisma (or any driver) with zero middleman API layer.

It Can Gate Access

Middleware and cookie-based sessions handle authentication and route protection.

The Core Insight

None of these four things are new concepts in this lesson — you've already built each one individually. What makes Next.js "full-stack" is that they all live in the same app/ folder, share the same TypeScript types, and deploy as a single unit.

The Three Backend Building Blocks, Side by Side

Building BlockEquivalent ToUse It For
Route Handlers (route.ts)An Express route (app.get, app.post)A public API consumed by mobile apps, webhooks, or third parties
Server Actions ("use server")A backend controller function called via fetchMutations triggered from your own app's forms and buttons
Middleware (middleware.ts)Express middleware (app.use)Auth checks, redirects, and header rewriting before a request completes

Traditional Stack vs Next.js Full-Stack

Traditional Split Stack (e.g. React + Express)

  • Two separate repositories (or two folders with separate configs)
  • Two deploys — the frontend build and the backend server
  • A CORS configuration to let the frontend call the backend
  • Duplicate type definitions for shared data shapes (or a shared package to maintain)
  • A network hop for every single data fetch, even in development

▲ Next.js Full-Stack

  • One repository, one app/ folder for both UI and API
  • One deploy — frontend and backend ship together
  • No CORS needed for your own frontend calling your own backend
  • Shared TypeScript types between a Server Action and the component that calls it
  • Server Components can skip the network hop and query the database directly

Setting Up the Backend, Step by Step

Here is the complete, minimal setup for a Next.js project that has real backend capability — a database, a public API, and a protected route — combining everything from earlier lessons into one sequence.

# 1. Scaffold the project (App Router, as covered in Lesson 6)
npx create-next-app@latest my-fullstack-app
cd my-fullstack-app
# 2. Add a database layer
npm install prisma @prisma/client
npx prisma init
// 3. Define your data — prisma/schema.prisma
model Task {
id String @id @default(cuid())
title String
done Boolean @default(false)
}
npx prisma migrate dev --name init
npx prisma generate
// 4. lib/prisma.ts — one shared client (see the database-integration lesson)
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// 5. A public API — app/api/tasks/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET() {
const tasks = await prisma.task.findMany();
return NextResponse.json(tasks);
}
// 6. A mutation from your own UI — app/actions.ts
'use server';
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function addTask(formData: FormData) {
await prisma.task.create({ data: { title: String(formData.get('title')) } });
revalidatePath('/tasks');
}
// 7. Gate a section behind auth — middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request) {
const isLoggedIn = request.cookies.has('session');
if (!isLoggedIn && request.nextUrl.pathname.startsWith('/tasks')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = { matcher: ['/tasks/:path*'] };
That's the Whole Backend

A database, a public JSON API, a server-side mutation, and a protected route — all inside one project, no separate backend service to stand up or deploy.

A Complete Request, End to End

Tracing one visitor loading /tasks shows every layer working together:

Request hits middleware.ts (session check)
↓
app/tasks/page.tsx (Server Component) runs
↓
It queries Prisma directly — no API call needed
↓
HTML streams back with the task list
↓
Submitting the form calls the addTask Server Action
↓
revalidatePath refreshes the page's cached data

What Next.js Still Doesn't Replace

Being full-stack for your own app's needs isn't the same as replacing every backend service you might ever need. Be realistic about the boundary.

Next.js Handles Well

  • CRUD APIs and page-driven mutations
  • Session-based authentication
  • Server-rendered, SEO-sensitive pages
  • Small-to-medium relational data workloads via Prisma

You'll Still Reach for Something Else

  • Long-running background jobs (use a queue service)
  • Heavy compute or ML workloads (a dedicated service)
  • Real-time features at scale (a dedicated WebSocket/Pusher service)
  • Multiple independent teams needing separately deployable services (microservices)

Common Beginner Mistakes

Building a Route Handler AND a Server Action for the same internal mutation

Pick one — Server Actions are simpler for mutations triggered by your own UI; reserve Route Handlers for things an external client actually needs to call.

Assuming "full-stack" means no backend knowledge is needed

You still need to understand databases, authentication, and API design — Next.js removes the project-setup overhead, not the underlying backend concepts.

FAQs

Not Express specifically, but the underlying concepts — HTTP methods, request/response, middleware, databases — are exactly the same skills, just expressed through Next.js's own conventions.

Yes for the vast majority of apps — Route Handlers and Server Actions deploy as serverless or edge functions on platforms like Vercel, scaling automatically with traffic.

Conceptually similar goals (JavaScript everywhere, a database, an API), but Next.js consolidates the "React app" and "Express server" into a single project instead of two.

Summary

Next.js is "full-stack" because Route Handlers, Server Actions, direct database access, and Middleware together cover everything a separate backend service used to be responsible for — all inside one project, one deploy, and one set of shared types. Next, you'll add TypeScript itself to the project for end-to-end type safety across that whole stack.

Next Lesson →

TypeScript with Next.js