What is a backend? Basic
The frontend is what users see. The backend is the part they don't see. It receives requests, applies business rules, talks to the database and sends back responses.
Your frontend and backend live in the same project and are deployed together.
Three ways to run server code in Next.js
Server Components
Read data directly while rendering a page. Best for GET-style reads.
Server Actions
Functions marked "use server". Best for mutations from your own forms.
Route Handlers
app/api/**/route.ts. A real REST API for mobile apps, webhooks and third parties.
HTTP & REST basics Basic
Every API call is an HTTP request made of a method, a URL, headers and an optional body. The server answers with a status code, headers and a body (usually JSON).
| Method | Meaning | Example |
|---|---|---|
| GET | Read | GET /api/products |
| POST | Create | POST /api/products |
| PUT/PATCH | Update (full/partial) | PATCH /api/products/7 |
| DELETE | Delete | DELETE /api/products/7 |
| Status | Meaning |
|---|---|
200 / 201 | OK / Created |
204 | No content (deleted) |
400 / 422 | Bad input / validation failed |
401 / 403 | Not logged in / not allowed |
404 / 409 | Not found / conflict (duplicate) |
500 | Server bug |
Route Handlers (API routes) Basic
Create a route.ts file inside app/ and export functions named after HTTP methods.
route.ts and page.tsx in the same folder. Keep APIs under app/api/.Reading requests & sending responses Basic
| Need | Code |
|---|---|
| JSON body | await req.json() |
| Form body | await req.formData() |
| Query string | req.nextUrl.searchParams.get("q") |
| JSON response + status | NextResponse.json(data, { status: 201 }) |
| Empty response | new Response(null, { status: 204 }) |
| Redirect | NextResponse.redirect(new URL("/login", req.url)) |
Connecting to PostgreSQL Basic
We'll use pg (node-postgres), the standard PostgreSQL driver for Node.js. If you haven't set up a database yet, start with the PostgreSQL notes.
- Install the driver
npm install pgandnpm install -D @types/pg - Add the connection stringPut it in
.env.local. - Create one shared connection poolCreate it once in
lib/db.tsand import it everywhere.
Opening a new connection for every query is slow, and Postgres limits how many connections it accepts. A pool keeps a few open and shares them.
lib/db.ts from server code: Route Handlers, Server Actions and Server Components. Add import "server-only"; at the top, and the build will fail if a Client Component imports it by mistake.Build a full CRUD API Intermediate
CRUD = Create, Read, Update, Delete. Here's a complete products API.
| Endpoint | File | SQL |
|---|---|---|
| GET /api/products | api/products/route.ts | SELECT |
| POST /api/products | api/products/route.ts | INSERT … RETURNING * |
| GET /api/products/:id | api/products/[id]/route.ts | SELECT … WHERE id=$1 |
| PATCH /api/products/:id | api/products/[id]/route.ts | UPDATE … RETURNING * |
| DELETE /api/products/:id | api/products/[id]/route.ts | DELETE |
pg returns NUMERIC/DECIMAL values as strings so they don't lose precision. Convert them with Number(row.price) only when that's safe, or use a decimal library for money.SQL injection — the #1 mistake Intermediate
If you glue user input into a SQL string, an attacker can type SQL that changes your query.
'; DROP TABLE users; --$1, $2… placeholders. You can't parameterize identifiers such as table names, column names or ORDER BY direction. For those, check the input against an allow-list: ["name","price"].includes(sort).Validating input with Zod Intermediate
Never trust the client. Zod checks the shape of the data at runtime and gives you TypeScript types from the same schema.
If validation fails, return 400 / 422 with the error list. Bad data never reaches the database.
Server Actions with the database Intermediate
When the only caller is your own UI, a Server Action is usually simpler than an API route. You don't need to write a fetch or define a URL.
| Use… | When |
|---|---|
| Server Component | Reading data to show on a page |
| Server Action | Forms and mutations triggered from your own Next.js UI |
| Route Handler | Public/external API, mobile apps, webhooks (Stripe, GitHub), file downloads, client-side fetching with SWR |
Using an ORM (Prisma) Intermediate
An ORM (Object-Relational Mapper) lets you query the database with typed JavaScript methods instead of raw SQL strings. It also manages migrations, which are version-controlled changes to your tables.
Raw SQL (pg)
- Full control, fastest
- You learn real SQL
- Manual types & migrations
ORM (Prisma / Drizzle)
- Auto-complete and type safety
- Migrations built in
- Complex queries can be awkward
prisma.config.ts file and driver adapters (@prisma/adapter-pg). The schema.prisma models and the query API shown above stay the same. Follow the setup guide for the version you install. Drizzle ORM is a popular SQL-like alternative.Error handling Intermediate
Return consistent JSON errors, map known database errors to the right status codes, and never show stack traces to users.
| PG code | Name | HTTP |
|---|---|---|
23505 | unique_violation | 409 Conflict |
23503 | foreign_key_violation | 400 / 409 |
23502 | not_null_violation | 400 |
22P02 | invalid_text_representation (e.g. "abc" as INT) | 400 |
Authentication (hash + JWT cookie) Advanced
Protecting routes: proxy / middleware Advanced
A proxy (called middleware before Next.js 16) runs before a request reaches your page or API. Use it for quick checks such as redirecting logged-out users.
Proxy is an optimistic first gate. Always check auth again where the data is read or changed.
Transactions Advanced
A transaction groups several queries so that all succeed or none do. A classic example is placing an order: create the order, reduce stock and charge the wallet. If any step fails, everything is undone.
Pagination, sorting & filtering Advanced
Offset pagination
LIMIT 20 OFFSET 40- Easy and supports "page 3 of 10"
- Gets slow on deep pages, and rows can shift between requests
Cursor (keyset) pagination
WHERE id < $last ORDER BY id DESC LIMIT 20- Fast at any depth because it uses the index
- Good for infinite scroll; no "jump to page"
Performance & scaling Advanced
Avoid N+1 queries
Don't run one query per item inside a loop. Use one JOIN or WHERE id = ANY($1) instead.
Add indexes
Index the columns you filter or join on. Check with EXPLAIN ANALYZE (see the PostgreSQL page).
Pooling in serverless
Serverless functions can open too many connections. Use PgBouncer or a pooled URL (Neon, Supabase).
Cache reads
Cache data that rarely changes with Next.js caching (revalidate / "use cache") or Redis.
Select only needed columns
Avoid SELECT * on wide tables in hot paths.
Background jobs
Send emails and process images in a queue so the request isn't blocked.
Security checklist Advanced
- Parameterized queries everywhere
- Validate every input (Zod)
- Hash passwords with bcrypt or argon2, never store them in plain text
- Put sessions in httpOnly + secure + sameSite cookies
- Check auth and ownership:
WHERE id=$1 AND user_id=$2
- Rate-limit login and public endpoints
- Keep secrets in env vars, never in
NEXT_PUBLIC_ - Use a DB user with minimum privileges (not superuser)
- Return generic error messages and log the details server-side
- Verify webhook signatures (Stripe and others)
/api/orders/55, check that order 55 belongs to A: SELECT * FROM orders WHERE id=$1 AND user_id=$2.Production project architecture Advanced
As your app grows, split the code into layers, so route files stay thin and the business logic can be tested.
- src/
- app/ pages + api routes + actions (thin)
- server/
- services/ business logic
- repositories/ SQL queries
- db.ts pool
- auth.ts
- lib/validators/ zod schemas (shared with the frontend)
- components/
- migrations/ numbered SQL files or Prisma migrations
Interview questions
What is the correct way to stop SQL injection?
revalidatePath. A Route Handler is an HTTP endpoint with a URL, for external clients, webhooks and client-side fetch.sameSite and secure to reduce CSRF risk and keep the cookie on HTTPS only.Practice projects
- Notes APICRUD with
pg, Zod validation and consistent errors. - Auth systemRegister, login and logout with bcrypt + JWT cookie, plus a protected
/dashboardusing proxy. - Mini e-commerceProducts, cart and orders, using a transaction to place each order. Add pagination and filters.
Cheat sheet
Route Handler
export async function GET()methodawait req.json()bodyreq.nextUrl.searchParamsqueryawait params[id]NextResponse.json(d,{status})reply
pg
new Pool({connectionString})pooldb.query(sql, [v])queryRETURNING *get row backdb.connect()tx clientclient.release()return it
Auth
bcrypt.hash(pw, 12)hashnew SignJWT().sign()tokenjwtVerify(t, secret)verify(await cookies()).set()cookie
Actions
"use server"mark filerevalidatePath("/x")refreshredirect("/x")navigateaction.bind(null, id)pass args