How to Build a Full Stack App with Next.js and NestJS in 2026
If you're a full stack developer in 2026, the Next.js + NestJS combination is one of the most powerful stacks you can use. You get React's component model on the frontend, NestJS's enterprise-grade architecture on the backend, and TypeScript everywhere.
In this guide, I'll walk you through the architecture I use for building production-ready full stack applications — the same approach I've used at my internships working on real products.
Why This Stack?
Before diving in, let's understand why Next.js + NestJS is such a strong combination:
| Layer | Technology | Why |
|---|---|---|
| Frontend | Next.js 16 | Server-side rendering, static generation, file-based routing, built-in optimizations |
| Backend | NestJS | Modular architecture, dependency injection, decorators, built-in validation |
| Language | TypeScript | End-to-end type safety, shared interfaces between frontend and backend |
| Database | PostgreSQL | Reliable, scalable, excellent for relational data |
| ORM | Prisma | Type-safe queries, migrations, intuitive schema definition |
This is the tech stack I work with daily, and it scales beautifully from side projects to production applications.
Project Architecture
Here's how I structure a typical full stack project:
project/
├── apps/
│ ├── web/ # Next.js frontend
│ │ ├── src/
│ │ │ ├── app/
│ │ │ ├── components/
│ │ │ ├── lib/
│ │ │ └── types/
│ │ └── package.json
│ └── api/ # NestJS backend
│ ├── src/
│ │ ├── modules/
│ │ ├── common/
│ │ └── main.ts
│ └── package.json
├── packages/
│ └── shared/ # Shared TypeScript types
│ ├── src/
│ │ └── types/
│ └── package.json
└── package.json # Root workspace
The key insight here is the shared types package. By defining your API request/response types in a shared package, you get compile-time guarantees that your frontend and backend agree on the data contract.
Setting Up the Backend with NestJS
1. Initialize the Project
nest new api
cd api
npm install @nestjs/config @nestjs/typeorm typeorm pg class-validator class-transformer
2. Create a Module
NestJS uses a modular architecture. Each feature gets its own module with a controller, service, and DTOs:
// src/modules/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
3. Define the Entity
// src/modules/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ select: false })
password: string;
@CreateDateColumn()
createdAt: Date;
}
4. Build the Service
// src/modules/users/users.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepo: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
const exists = await this.userRepo.findOneBy({ email: dto.email });
if (exists) throw new ConflictException('Email already in use');
const user = this.userRepo.create(dto);
return this.userRepo.save(user);
}
async findAll(): Promise<User[]> {
return this.userRepo.find();
}
}
5. Create the Controller
// src/modules/users/users.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
}
Setting Up the Frontend with Next.js
1. API Client
Create a typed API client that uses the shared types:
// src/lib/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
export async function apiFetch<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const res = await fetch(`${API_URL}${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!res.ok) {
const error = await res.json().catch(() => ({}));
throw new Error(error.message ?? 'API request failed');
}
return res.json();
}
2. Server Components for Data Fetching
One of Next.js's biggest strengths is server components — fetch data directly in your components without useEffect:
// src/app/users/page.tsx
import { apiFetch } from '@/lib/api';
import type { User } from '@shared/types';
export default async function UsersPage() {
const users = await apiFetch<User[]>('/users');
return (
<div>
<h1>Users</h1>
{users.map((user) => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
))}
</div>
);
}
No loading states, no useEffect, no client-side fetching. The data is fetched at build time or request time on the server.
Authentication Flow
For auth, I typically use JWT with refresh tokens:
- Login → Backend validates credentials, returns
accessToken+refreshToken - Access Token → Short-lived (15 min), stored in memory
- Refresh Token → Long-lived (7 days), stored in HTTP-only cookie
- Middleware → NestJS Guards protect routes, Next.js middleware protects pages
// NestJS Guard
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) throw new UnauthorizedException();
try {
const payload = this.jwtService.verify(token);
request.user = payload;
return true;
} catch {
throw new UnauthorizedException();
}
}
}
Database Migrations
Never modify your production database manually. Use migrations:
# Generate a migration from entity changes
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddUserTable
# Run migrations
npx typeorm migration:run -d src/data-source.ts
Deployment
My typical deployment setup:
- Frontend (Next.js) → Vercel (zero-config, automatic)
- Backend (NestJS) → Railway or Render (Docker-based)
- Database (PostgreSQL) → Supabase or Neon (managed Postgres)
Key Takeaways
- Share types between frontend and backend for end-to-end type safety
- Use server components in Next.js to eliminate client-side data fetching
- Modular architecture in NestJS keeps code organized as it scales
- JWT + HTTP-only cookies for secure authentication
- Database migrations for safe schema changes
This is the architecture behind my projects like Reparo and BlogVerse. If you have questions about building full stack applications, feel free to reach out.
Abhishek Sharma
Full Stack Engineer