Variables & Types
Declaration
let x = 5; // reassignable
const PI = 3.14; // constant
var old = 'avoid'; // function-scoped
Types
// Primitives
typeof 42 // "number"
typeof "hi" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ⚠️ bug
typeof Symbol() // "symbol"
typeof 42n // "bigint"
// Reference
typeof {} // "object"
typeof [] // "object" (use Array.isArray)
typeof fn // "function"
Truthy / Falsy
// Falsy: false, 0, "", null,
// undefined, NaN
// Everything else is truthy
Scope & Closures
Hoisting
// var declarations hoisted (not value)
console.log(x); // undefined
var x = 5;
// let/const: TDZ error if used early
// Function declarations hoisted fully
greet(); // works!
function greet() { ... }
Closure
function counter() {
let count = 0;
return () => ++count;
}
const inc = counter();
inc(); // 1
inc(); // 2
// Inner fn "closes over" count
Scope Chain
let outer = 'A';
function fn() {
let inner = 'B';
// can access outer, not vice versa
}
Functions & this
Function Types
// Declaration
function add(a, b) { return a + b; }
// Expression
const add = function(a, b) { ... };
// Arrow (no own `this`)
const add = (a, b) => a + b;
// Default params
function greet(name = 'World') {
return `Hello ${name}`;
}
// Rest params
function sum(...nums) {
return nums.reduce((a, b) => a + b);
}
this Binding
// Regular fn: this = caller
// Arrow fn: this = surrounding scope
const obj = { name: 'Bob',
greet() { return this.name; }
};
obj.greet(); // "Bob"
// Explicit bind:
fn.call(ctx, a, b)
fn.apply(ctx, [a, b])
const bound = fn.bind(ctx);
ES6+ Essentials
Destructuring
// Object
const { name, age = 0 } = user;
const { name: n } = user; // rename
// Array
const [first, second, ...rest] = arr;
// In function params
function show({ name, age }) { ... }
Spread & Rest
// Spread: expand
const arr2 = [...arr1, 4, 5];
const obj2 = { ...obj1, age: 30 };
Math.max(...nums);
// Rest: collect remaining
const [head, ...tail] = [1,2,3];
Template Literals
const msg = `Hello ${name}, you are ${age} years old`;
// Multi-line — just press Enter inside backticks
Short-circuit
const val = a ?? 'default'; // null/undef
const val = a || 'fallback'; // falsy
user?.address?.city // optional chain
Arrays & Iteration
Core Methods
const nums = [1,2,3,4,5];
// map — transform each element
nums.map(x => x * 2);
// [2,4,6,8,10]
// filter — keep matching elements
nums.filter(x => x > 2);
// [3,4,5]
// reduce — accumulate to one value
nums.reduce((sum, x) => sum + x, 0);
// 15
// find / findIndex
nums.find(x => x > 3); // 4
nums.findIndex(x => x===3);// 2
// some / every
nums.some(x => x > 4); // true
nums.every(x => x > 0); // true
// flat / flatMap
[[1],[2]].flat(); // [1,2]
nums.flatMap(x=>[x,x*2]);
Mutation Methods
arr.push(v) // add end
arr.pop() // remove end
arr.unshift(v) // add start
arr.shift() // remove start
arr.splice(i,n) // remove n from i
arr.sort((a,b)=>a-b) // numeric sort
arr.reverse()
Objects & Classes
Object Patterns
const name = 'Alice';
const user = {
name, // shorthand
greet() { ... }, // method shorthand
['key']: 'dynamic' // computed key
};
// Useful Object methods
Object.keys(user) // ["name","greet"]
Object.values(user) // values array
Object.entries(user) // [[k,v],...]
Object.assign({}, a, b) // merge
Object.freeze(obj) // immutable
ES6 Classes
class Animal {
#name; // private field
constructor(name) {
this.#name = name;
}
speak() {
return `${this.#name} makes a sound`;
}
}
class Dog extends Animal {
speak() {
return super.speak() + ' Woof!';
}
}
Map & Set
const m = new Map();
m.set('a', 1); m.get('a');
const s = new Set([1,2,2,3]);
// Set {1, 2, 3} — unique values
const unique = [...new Set(arr)];
Async JavaScript
Promises
const p = new Promise((resolve, reject) => {
// async work...
resolve(data); // success
reject(error); // failure
});
p.then(data => ...)
.catch(err => ...)
.finally(() => ...);
// Multiple promises
Promise.all([p1, p2]) // all succeed
Promise.race([p1, p2]) // first settles
Promise.allSettled([...])// all settle
Async / Await
async function fetchUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(res.status);
const data = await res.json();
return data;
} catch (err) {
console.error('Error:', err);
}
}
// Parallel with async/await
const [a, b] = await Promise.all([p1(), p2()]);
Event Loop
// Call Stack → Web APIs → Task Queue
// Microtasks (Promises) before macrotasks
// (setTimeout, setInterval)
setTimeout(() => /*macro*/, 0);
Promise.resolve().then(() => /*micro*/);
DOM & Browser
Selecting Elements
document.querySelector('#id');
document.querySelectorAll('.cls');
document.getElementById('id');
Manipulation
el.textContent = 'text';
el.innerHTML = '<b>bold</b>';
el.classList.add('active');
el.classList.toggle('hidden');
el.setAttribute('href', '/path');
el.style.color = 'red';
parent.appendChild(child);
el.remove();
Events
el.addEventListener('click', (e) => {
e.preventDefault(); // stop default
e.stopPropagation(); // stop bubble
e.target; // element clicked
});
// Event delegation — listen on parent:
list.addEventListener('click', e => {
if (e.target.matches('li')) { ... }
});
Storage & Utilities
localStorage.setItem('k', JSON.stringify(v));
localStorage.getItem('k');
// Debounce: wait until user stops typing
const debounce = (fn, ms) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
};
Components & JSX
Functional Component
// Always start with capital letter
function UserCard({ name, age, onClick }) {
return (
<div className="card">
<h2>{name}</h2>
<p>Age: {age}</p>
<button onClick={onClick}>Click</button>
</div>
);
}
// Export
export default UserCard;
export { UserCard }; // named
JSX Rules
// 1. Single root element (or Fragment)
return (
<>
<A /><B />
</>
);
// 2. className, not class
// 3. htmlFor, not for
// 4. Self-close empty tags <br />
// 5. JS in {curly braces}
// 6. Conditional rendering
{isLoggedIn && <Dashboard />}
{isLoggedIn ? <A /> : <B />}
// 7. Lists must have key prop
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
Core Hooks
useState
const [count, setCount] = useState(0);
// Never mutate state directly!
setCount(count + 1);
setCount(prev => prev + 1);
// Objects: spread to update
setUser(prev => ({ ...prev, age: 30 }));
// Arrays: use map/filter (no mutation)
setItems(prev => [...prev, newItem]);
setItems(prev => prev.filter(i=>i.id!==id));
useEffect
// Runs after every render
useEffect(() => { ... });
// Runs once (mount)
useEffect(() => { ... }, []);
// Runs when dep changes
useEffect(() => { ... }, [userId]);
// Cleanup (unmount / before re-run)
useEffect(() => {
const timer = setInterval(fn, 1000);
return () => clearInterval(timer);
}, []);
// Fetch data pattern
useEffect(() => {
async function load() {
const data = await fetchUsers();
setUsers(data);
}
load();
}, []);
More Hooks & Patterns
useContext
// 1. Create context
const ThemeCtx = React.createContext(null);
// 2. Provide at top level
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeCtx.Provider value={{ theme, setTheme }}>
<Page />
</ThemeCtx.Provider>
);
}
// 3. Consume anywhere
const { theme } = useContext(ThemeCtx);
useReducer
const reducer = (state, action) => {
switch (action.type) {
case 'INC': return { ...state, count: state.count+1 };
default: return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: 'INC' });
useMemo & useCallback
// useMemo — memoize expensive value
const sorted = useMemo(
() => items.sort(...),
[items]
);
// useCallback — memoize function ref
const handleClick = useCallback(
() => { ... },
[dep]
);
// React.memo — skip re-render if props same
export default React.memo(MyComponent);
Redux Toolkit
Setup Store
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: { counter: counterReducer },
});
// index.js — wrap App
<Provider store={store}>
<App />
</Provider>
Create Slice
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) { state.value++; },
decrement(state) { state.value--; },
addBy(state, action) {
state.value += action.payload;
},
},
});
export const { increment, decrement, addBy } =
counterSlice.actions;
export default counterSlice.reducer;
Use in Component
import { useSelector, useDispatch } from 'react-redux';
import { increment, addBy } from './counterSlice';
function Counter() {
const count = useSelector(s => s.counter.value);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(addBy(5))}>
{count}
</button>
);
}
Lifecycle → useEffect Map
// componentDidMount (run once on mount)
useEffect(() => { fetchData(); }, []);
// componentDidUpdate (run on dep change)
useEffect(() => { updateTitle(); }, [title]);
// componentWillUnmount (cleanup)
useEffect(() => {
const sub = subscribe();
return () => unsubscribe(sub);
}, []);
// getDerivedStateFromProps — useMemo / derive from props
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);
// Error Boundary (still class-based in React 18)
class ErrBoundary extends React.Component {
componentDidCatch(error, info) { ... }
render() {
return this.state.hasError
? <h1>Something broke</h1>
: this.props.children;
}
}
Custom Hook + Async Redux (Thunk)
Custom Hook — reusable logic
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(r => r.json()).then(setData)
.catch(setError).finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage: const { data, loading } = useFetch('/api/users');
createAsyncThunk (async Redux)
export const fetchUsers = createAsyncThunk(
'users/fetch',
async (id, { rejectWithValue }) => {
try { return await api.getUsers(id); }
catch (e) { return rejectWithValue(e.message); }
}
);
// In slice extraReducers:
builder
.addCase(fetchUsers.pending, s => { s.loading=true; })
.addCase(fetchUsers.fulfilled, (s, a) => {
s.loading = false; s.users = a.payload;
})
.addCase(fetchUsers.rejected, (s, a) => {
s.error = a.payload;
});
Core Query Structure
-- Execution order (not write order):
-- FROM → JOIN → WHERE → GROUP BY
-- → HAVING → SELECT → ORDER BY → LIMIT
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 2
ORDER BY order_count DESC
LIMIT 10;
WHERE Conditions
WHERE age BETWEEN 18 AND 65
WHERE country IN ('VN', 'SG', 'TH')
WHERE name LIKE 'A%' -- starts with A
WHERE name ILIKE '%alice%'-- case-insensitive
WHERE email IS NULL
WHERE email IS NOT NULL
WHERE NOT (status = 'inactive')
CASE WHEN
SELECT name,
CASE
WHEN score >= 90 THEN 'A'
WHEN score >= 80 THEN 'B'
ELSE 'C'
END AS grade
FROM students;
JOIN Patterns
Join Types
-- INNER JOIN: only matching rows
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- LEFT JOIN: all from left + matched right
SELECT u.name, o.id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- Unmatched rows only (anti-join)
WHERE o.id IS NULL;
-- Self-join: compare rows in same table
SELECT e.name, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
GROUP BY Patterns
-- Count per group
SELECT country, COUNT(*) FROM users
GROUP BY country;
-- Find duplicates
SELECT email, COUNT(*)
FROM users GROUP BY email
HAVING COUNT(*) > 1;
-- Aggregates
COUNT(*) · SUM(col) · AVG(col)
MIN(col) · MAX(col)
COUNT(DISTINCT col)
Subqueries & Window Fns
Subquery Patterns
-- IN subquery
SELECT * FROM products
WHERE category_id IN (
SELECT id FROM categories
WHERE active = true
);
-- Scalar subquery
SELECT name,
(SELECT COUNT(*) FROM orders o
WHERE o.user_id = u.id) AS total
FROM users u;
-- EXISTS
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
);
Window Functions
-- Syntax: fn() OVER (PARTITION BY .. ORDER BY ..)
-- Row number per partition
SELECT name, score,
ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY score DESC
) AS rank
FROM employees;
-- Running total
SUM(amount) OVER (ORDER BY created_at)
-- Lag/Lead (previous/next row)
LAG(salary, 1) OVER (ORDER BY hire_date)
Data Modification & CTEs
INSERT / UPDATE / DELETE
-- INSERT
INSERT INTO users (name, email, age)
VALUES ('Alice', 'a@b.com', 28)
RETURNING id; -- get back inserted ID
-- INSERT multiple rows
INSERT INTO tags (name) VALUES
('js'), ('react'), ('sql');
-- UPDATE
UPDATE users
SET age = 29, updated_at = NOW()
WHERE id = 5;
-- DELETE
DELETE FROM sessions
WHERE expired_at < NOW();
CTE (WITH clause)
-- Readable, reusable named subquery
WITH top_users AS (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000
)
SELECT u.name, t.total
FROM users u
JOIN top_users t ON u.id = t.user_id;
Top N per Group
-- Top 1 score per department
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY dept_id
ORDER BY score DESC
) AS rn
FROM employees
)
SELECT * FROM ranked WHERE rn = 1;
File & Dir Management
Navigation
| pwd | Print current directory |
| ls -la | List all (hidden + details) |
| cd /path | Change to absolute path |
| cd .. | Go up one level |
| cd ~ | Go to home directory |
| cd - | Go to previous directory |
Create / Copy / Move / Delete
| mkdir -p a/b/c | Create nested dirs |
| touch file.txt | Create empty file |
| cp src dst | Copy file |
| cp -r src/ dst/ | Copy directory |
| mv old new | Move / rename |
| rm file.txt | Delete file |
| rm -rf dir/ | Force delete dir |
View File Content
| cat file | Print entire file |
| less file | Paginated view (q to quit) |
| head -n 20 file | First 20 lines |
| tail -f log.txt | Follow log in real-time |
| wc -l file | Count lines |
Search & Text Processing
find — locate files
# Find by name
find . -name "*.js"
# Find by type (f=file, d=dir)
find . -type f -name "*.log"
# Find & execute command
find . -name "*.tmp" -delete
# Find modified in last 7 days
find . -mtime -7
grep — search in files
# Basic search
grep "error" app.log
# Case-insensitive
grep -i "error" app.log
# Recursive in all files
grep -r "TODO" src/
# Show line number
grep -n "function" app.js
# Invert (lines NOT matching)
grep -v "debug" app.log
# With regex
grep -E "error|warn" app.log
sort, uniq, cut, awk
cat f | sort | uniq -c # count unique
cut -d',' -f1 data.csv # 1st CSV column
awk '{print $2}' file # 2nd field
sed 's/foo/bar/g' file # replace text
Permissions & Processes
chmod — change permissions
# Octal: r=4, w=2, x=1
# 755 = rwxr-xr-x
# 644 = rw-r--r--
chmod 755 script.sh # owner=rwx group=r-x other=r-x
chmod +x script.sh # add execute for all
chmod -R 644 dir/ # recursive
chown user:group file # change owner
Process Management
| ps aux | List all processes |
| top / htop | Live process monitor |
| kill -9 PID | Force kill by PID |
| pkill node | Kill by name |
| jobs | List background jobs |
| cmd & | Run in background |
| fg %1 | Bring job 1 to foreground |
| Ctrl+C | Interrupt current process |
| Ctrl+Z | Suspend process |
Disk & Memory
| df -h | Disk usage (human readable) |
| du -sh dir/ | Directory size |
| free -h | RAM usage |
Networking & Shell Tips
Network Commands
| ip addr show | Show IP addresses |
| ping google.com | Test connectivity |
| curl -X GET url | HTTP GET request |
| curl -X POST -d '{}' url | POST with data |
| wget url | Download file |
| ssh user@host | SSH into server |
| scp f user@h:/path | Secure copy to server |
| netstat -tulpn | Open ports |
Pipes & Redirection
# Pipe: send output to next command
cat f.txt | grep "err" | wc -l
# Redirect output to file
echo "hello" > out.txt # overwrite
echo "hello" >> out.txt # append
cmd 2> err.log # stderr to file
cmd > out.txt 2>&1 # both to file
Variables & Scripts
#!/bin/bash
NAME="World"
echo "Hello $NAME"
# Conditionals
if [ -f "file.txt" ]; then
echo "exists"
fi
# Loop
for f in *.js; do
echo "$f"
done
Common Flags Memory Aid
-h human-readable -r recursive
-f force -v verbose
-i interactive -n dry run / line no
-a all (hidden) -l long format
Request Lifecycle — Interview Gold
Request
→
Middleware
→
Guard
→
Interceptor (pre)
→
Pipe
→
Controller
→
Service
→
Interceptor (post)
→
Exception Filter
→
Response
Architecture & Bootstrap
Philosophy
| TypeScript-first | Built with & for TypeScript |
| Modular | Feature modules, shared modules |
| DI container | Inversion of Control built-in |
| Express / Fastify | Pluggable HTTP adapters |
main.ts — bootstrap
import { NestFactory } from '@nestjs/core';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({
whitelist: true, transform: true
}));
app.enableCors();
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();
Key Decorators
@Module({ imports, providers, controllers, exports })
@Controller('users') // route prefix
@Injectable() // DI provider
@Get() @Post() @Put(':id') @Delete(':id')
@Param('id') @Body() @Query() @Headers()
Modules & Dependency Injection
Module Structure
@Module({
imports: [TypeOrmModule, AuthModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // share
})
export class UsersModule {}
DI Scopes
| Singleton | One instance per app (default) |
| Request | New instance per HTTP request |
| Transient | New instance per injection |
Custom Providers
// inject a constant
{ provide: 'CFG', useValue: { port: 3000 } }
// swap class implementation
{ provide: Logger, useClass: WinstonLogger }
// async factory with injection
{ provide: 'DB',
useFactory: async (cfg) => createConn(cfg),
inject: [ConfigService] }
Request Pipeline
Guard — authorization
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(ctx: ExecutionContext) {
const req = ctx.switchToHttp().getRequest();
return validateToken(req.headers.authorization);
}
}
Pipe — validate & transform
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip extra
forbidNonWhitelisted: true, // throw on extra
transform: true, // auto-cast
}));
Exception Filter
@Catch(HttpException)
export class HttpExFilter implements ExceptionFilter {
catch(ex: HttpException, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
res.status(ex.getStatus()).json({ message: ex.message });
}
}
DTO & Validation
DTO with class-validator
import { IsEmail, IsNotEmpty, MinLength,
IsOptional } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty() name: string;
@IsEmail() email: string;
@MinLength(8) password: string;
@IsOptional() role?: string;
}
class-transformer
import { Exclude, Expose } from 'class-transformer';
export class UserResponseDto {
@Expose() id: number;
@Expose() email: string;
@Exclude() password: string; // hidden
}
Common Validators
| @IsString() | Must be a string |
| @IsNumber() | Must be a number |
| @IsEnum(Role) | Must be enum value |
| @IsArray() | Must be an array |
| @IsUUID() | Must be valid UUID |
Auth & Security
JWT + Passport Strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload) {
return { id: payload.sub, email: payload.email };
}
}
RBAC — Roles Guard
// decorator
export const Roles = (...r) => SetMetadata('roles', r);
// guard reads metadata via Reflector
const req = this.reflector.get('roles', ctx.getHandler());
Security Best Practices
| Helmet | HTTP security headers |
| ThrottlerModule | Rate limiting (DDoS) |
| bcrypt | Password hash ≥10 rounds |
| CORS | app.enableCors({ origin }) |
Data Access & Testing
ORM Comparison
| Prisma | Schema-first, type-safe client, migrations |
| TypeORM | Entity decorators, @InjectRepository |
| Mongoose | MongoDB, schema + model pattern |
| Drizzle | SQL-first, lightweight, type-safe |
Repository Pattern
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private repo: Repository<User>
) {}
findAll() { return this.repo.find(); }
}
Unit Testing
const module = await Test.createTestingModule({
providers: [UsersService, {
provide: getRepositoryToken(User),
useValue: { find: jest.fn() },
}]
}).compile();
Advanced Features
Microservices & Transports
| TCP | Simple message passing between services |
| Redis | Pub/Sub pattern via Redis transport |
| gRPC + Protobuf | High-performance binary RPC |
| Kafka / RabbitMQ | Event streaming & message queues |
GraphQL — code-first
@Resolver(() => User)
export class UsersResolver {
@Query(() => [User])
users() { return this.svc.findAll(); }
@Mutation(() => User)
createUser(@Args('input') i: CreateUserInput) {
return this.svc.create(i);
}
}
WebSocket Gateway
@WebSocketGateway({ cors: true })
export class EventsGateway {
@WebSocketServer() server: Server;
@SubscribeMessage('message')
handle(@MessageBody() data: string) {
this.server.emit('broadcast', data);
}
}
Production & Scaling
Performance
| Fastify adapter | ~2× throughput vs Express |
| SWC compiler | 10× faster than tsc build |
| Standalone app | createApplicationContext() |
Docker Multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./ && RUN npm ci
COPY . . && RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main"]
Observability
| OpenTelemetry | Traces, metrics, logs (OTLP) |
| Pino / Winston | Structured JSON logging |
| enableShutdownHooks() | SIGTERM → graceful close |
| ConfigModule + Joi/Zod | Env validation at startup |
Type Safety Compass — Interview Gold
any
opt-out → avoid
|
unknown
safe, must narrow
|
never
impossible / exhaustive
|
type
unions · aliases
|
interface
declaration merging
|
generic <T>
reusable + safe
|
strict: true
always enable
Type System Fundamentals
Primitives & Special Types
| string · number · boolean | Core primitives |
| null · undefined · bigint | Other primitives |
| any | Opt-out — avoid, use unknown |
| unknown | Safe any — narrow before use |
| never | Impossible type · exhaustive checks |
| void | Function returns undefined |
as const & satisfies
// as const — freeze to literal type
const ROLES = ['admin', 'user'] as const;
type Role = typeof ROLES[number];
// 'admin' | 'user'
// satisfies — validate without widening
const cfg = { port: 3000 } satisfies Config;
cfg.port; // type: 3000 (not number)
Strict Mode — tsconfig.json
{
"compilerOptions": {
"strict": true, // all safety flags
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Utility Types — Must Know
Structural
| Partial<T> | All properties optional |
| Required<T> | All properties required |
| Readonly<T> | All properties readonly |
| NonNullable<T> | Remove null & undefined |
Extraction
| Pick<T, K> | Keep listed keys |
| Omit<T, K> | Remove listed keys |
| Record<K, V> | Map from keys to values |
| Exclude<T, U> | Remove union members |
| Extract<T, U> | Keep matching members |
Function & Async
| ReturnType<F> | Function return type |
| Parameters<F> | Params as tuple |
| Awaited<T> | Unwrap Promise (recursive) |
type User = { id: number; name: string; pw: string };
type PublicUser = Omit<User, 'pw'>;
type UpdateUser = Partial<Pick<User, 'name'>>;
type UserMap = Record<number, User>;
Generics & Type Narrowing
Generic Functions
// Constraint — T must have .length
function first<T extends { length: number }>(arr: T) {
return arr.length;
}
// Type-safe property getter
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Type Narrowing
// typeof — primitives
if (typeof val === 'string') val.toUpperCase();
// instanceof — classes
if (err instanceof Error) err.message;
// in — duck typing
if ('swim' in animal) animal.swim();
// Type predicate — custom guard
function isString(v: unknown): v is string {
return typeof v === 'string';
}
keyof & typeof
type Keys = keyof User; // 'id' | 'name' | 'pw'
type Cfg = typeof config; // infer from value
type Emails = User['email']; // lookup type
Interfaces & Classes
type vs interface
| interface | Declaration merging · extends · implements |
| type | Unions · intersections · primitives · tuples |
// interface — extendable, mergeable
interface Animal { name: string }
interface Dog extends Animal { breed: string }
// type — unions (interface cannot)
type Status = 'active' | 'inactive';
type Admin = User & { permissions: string[] };
Class Access Modifiers
class User {
constructor(
public readonly id: number,
private name: string,
protected email: string,
) {}
}
// public — accessible anywhere
// private — class only (TS; use # for runtime)
// protected — class + subclasses
// readonly — set only in constructor
enum vs as const (prefer as const)
// as const — recommended, no runtime overhead
const STATUS = { Active: 'ACTIVE', Off: 'OFF' } as const;
type Status = typeof STATUS[keyof typeof STATUS];
Real-world Patterns
Discriminated Unions
type State =
| { kind: 'loading' }
| { kind: 'ok'; data: string[] }
| { kind: 'error'; msg: string };
switch (state.kind) {
case 'ok': return state.data;
case 'error': throw state.msg;
default: assertNever(state); // safe
}
Zod — Runtime + Compile-time
import { z } from 'zod';
const Schema = z.object({
id: z.number().positive(),
email: z.string().email(),
});
type User = z.infer<typeof Schema>; // no duplication
const result = Schema.safeParse(data);
if (result.success) result.data.email; // typed
Branded Types
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<number, 'UserId'>;
type ProductId = Brand<number, 'ProductId'>;
// getUser(productId) → ERROR! nominal safety
Advanced Types & Async
Mapped & Conditional Types
// Mapped — transform all keys
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Conditional — ternary at type level
type IsStr<T> = T extends string ? 'yes' : 'no';
// infer — extract inner type
type Unwrap<T> = T extends Promise<infer R> ? R : T;
type Deep<T> = { [K in keyof T]: T[K] extends object
? Deep<T[K]> : T[K] }; // recursive
Async / Promise Typing
async function fetchUser(id: number): Promise<User> {
const res = await fetch('/users/' + id);
return res.json() as Promise<User>;
}
// Error handling — catch e: unknown
try { await fetchUser(1); }
catch (e: unknown) {
if (e instanceof Error) e.message;
}
Build Tools
| tsc --noEmit | Type check only (CI gate) |
| tsx / ts-node | Run .ts directly (dev) |
| SWC / esbuild | 10x faster build (no type check) |
| tsup | Library bundler — CJS + ESM + .d.ts |
| Node 22+ --experimental-strip-types | No build step |
Big-O Complexity — Know These Cold
O(1)
Map/Set/Object lookup · push/pop · arithmetic
|
O(log n)
binary search · BST avg · heap insert
|
O(n)
scan · BFS/DFS · hash build
|
O(n log n)
merge/quick sort · Array.sort
|
O(n²)
nested loops · bubble/insertion sort
|
O(2ⁿ)
backtracking · subset enumeration
Most Frequent Coding Patterns
Sliding Window
Two Pointers
HashMap / Freq Counter
Binary Search
BFS / DFS
Prefix Sum
Monotonic Stack
Memo / DP
Backtracking
Fast & Slow Pointers
Linear Structures
Array — operation costs
| push / pop | O(1) amortized |
| shift / unshift | O(n) — avoid in hot loops |
| splice / slice | O(n) |
| indexOf / includes | O(n) linear scan |
| arr[i] | O(1) random access |
Stack — LIFO (array-based)
const stack = [];
stack.push(x); // O(1)
stack.pop(); // O(1)
stack[stack.length-1]; // peek O(1)
// use: valid parentheses, monotonic stack
Queue — FIFO (optimized)
class Queue {
#data = {}; #head = 0; #tail = 0;
enqueue(v) { this.#data[this.#tail++] = v; }
dequeue() { return this.#data[this.#head++]; }
size() { return this.#tail - this.#head; }
}
// O(1) enqueue AND dequeue — use for BFS
Linked List node
class ListNode {
constructor(val, next = null) {
this.val = val; this.next = next;
}
}
// key: reverse · cycle (fast+slow) · middle
Hash Structures — O(1) Power
Map vs Set vs Object
| Map | Any key · insertion order · .size |
| Set | Unique values · O(1) has/add/delete |
| {} | String/Symbol keys · prototype chain risk |
// Frequency counter — char count
const freq = new Map();
for (const c of str)
freq.set(c, (freq.get(c) ?? 0) + 1);
// Two sum — O(n)
function twoSum(nums, target) {
const seen = new Map();
for (let i = 0; i < nums.length; i++) {
const comp = target - nums[i];
if (seen.has(comp)) return [seen.get(comp), i];
seen.set(nums[i], i);
}
}
Sliding Window — O(n)
// Longest substring without repeating chars
let l = 0, best = 0;
const win = new Set();
for (let r = 0; r < s.length; r++) {
while (win.has(s[r])) win.delete(s[l++]);
win.add(s[r]);
best = Math.max(best, r - l + 1);
}
Two Pointers — O(n)
// Container with most water
let l = 0, r = h.length - 1, best = 0;
while (l < r) {
best = Math.max(best, Math.min(h[l],h[r])*(r-l));
h[l] < h[r] ? l++ : r--;
}
Trees — Interview Favourite
TreeNode + DFS traversals
class TreeNode {
constructor(val, left=null, right=null) {
this.val=val; this.left=left; this.right=right;
}
}
// Recursive DFS (pre-order)
function dfs(node) {
if (!node) return;
visit(node); // pre-order
dfs(node.left);
// visit(node) // in-order
dfs(node.right);
// visit(node) // post-order
}
BFS — level order
function bfs(root) {
const q = [root]; const res = [];
while (q.length) {
const n = q.length;
const level = [];
for (let i=0; i<n; i++) {
const node = q.shift();
level.push(node.val);
if (node.left) q.push(node.left);
if (node.right) q.push(node.right);
}
res.push(level);
}
return res;
}
Classic tree problems
| Max depth | 1 + max(dfs(L), dfs(R)) |
| Diameter | max(L+R) across all nodes |
| LCA | bubble up when node = p or q |
| Path sum | DFS subtract target at each node |
| Validate BST | pass [min, max] bounds in DFS |
Graphs — BFS / DFS Mastery
Adjacency list (Map)
const graph = new Map();
function addEdge(u, v) {
if (!graph.has(u)) graph.set(u, []);
if (!graph.has(v)) graph.set(v, []);
graph.get(u).push(v);
graph.get(v).push(u); // undirected
}
BFS — shortest path / level order
function bfs(start) {
const visited = new Set([start]);
const queue = [start];
while (queue.length) {
const node = queue.shift();
for (const nb of graph.get(node) ?? []) {
if (!visited.has(nb)) {
visited.add(nb); queue.push(nb);
}
}
}
}
DFS — components / cycle
function dfs(node, visited) {
visited.add(node);
for (const nb of graph.get(node) ?? [])
if (!visited.has(nb)) dfs(nb, visited);
}
Classic graph problems
| Number of islands | BFS/DFS on grid, mark visited |
| Course schedule | Topological sort (Kahn's BFS) |
| Clone graph | BFS + Map (original→clone) |
| Shortest path | BFS (unweighted) · Dijkstra (weighted) |
Sorting & Binary Search
Array.sort() — Timsort O(n log n)
// Numeric sort — MUST provide comparator
arr.sort((a, b) => a - b); // asc
arr.sort((a, b) => b - a); // desc
// String sort (default = lexicographic)
arr.sort((a, b) => a.localeCompare(b));
// Gotcha: [10,9,2].sort() → [10,2,9] !
Binary Search template — O(log n)
function binarySearch(arr, target) {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) {
const mid = lo + Math.floor((hi-lo)/2);
if (arr[mid] === target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
// Variants: first/last occurrence, rotated array
Merge sort — O(n log n), O(n) space
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const L = mergeSort(arr.slice(0, mid));
const R = mergeSort(arr.slice(mid));
return merge(L, R); // merge two sorted halves
}
Prefix Sum — O(n) preprocess, O(1) query
const pre = [0];
for (const n of nums) pre.push(pre.at(-1) + n);
// sum[i..j] = pre[j+1] - pre[i]
Heaps & Union-Find
Min-heap (array-based)
| parent(i) | Math.floor((i-1)/2) |
| left(i) | 2*i + 1 |
| right(i) | 2*i + 2 |
| insert | push → bubble up O(log n) |
| extractMin | swap root↔last → bubble down O(log n) |
// Top-K elements with size-K min-heap
// Simulate: sort desc + slice(0, K) for interview
nums.sort((a,b)=>b-a).slice(0,k); // O(n log n)
// Real heap gives O(n log k) — mention trade-off
Union-Find — path compression
class UF {
constructor(n) {
this.p = Array.from({length:n}, (_,i)=>i);
this.rank = new Array(n).fill(0);
}
find(x) {
if (this.p[x] !== x) this.p[x] = this.find(this.p[x]);
return this.p[x]; // path compression
}
union(x, y) {
const [px,py] = [this.find(x), this.find(y)];
if (px===py) return false;
this.rank[px] >= this.rank[py]
? this.p[py]=px : this.p[px]=py;
return true;
}
}
// ≈ O(α(n)) per op · use: islands, connections
Dynamic Programming
Recognize DP problems
| "min/max" | Optimal substructure |
| "count ways" | Overlapping subproblems |
| "is it possible" | Boolean DP |
Memoization — top-down
const memo = new Map();
function fib(n) {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const res = fib(n-1) + fib(n-2);
memo.set(n, res); return res;
}
Tabulation — bottom-up
// Coin change — min coins to make amount
function coinChange(coins, amount) {
const dp = new Array(amount+1).fill(Infinity);
dp[0] = 0;
for (let i=1; i<=amount; i++)
for (const c of coins)
if (c <= i) dp[i] = Math.min(dp[i], dp[i-c]+1);
return dp[amount] === Infinity ? -1 : dp[amount];
}
Classic DP patterns
| Climbing stairs | dp[i] = dp[i-1] + dp[i-2] |
| House robber | dp[i] = max(dp[i-1], dp[i-2]+nums[i]) |
| LIS | dp[i] = max(dp[j]+1) for j<i, arr[j]<arr[i] |
| 0/1 knapsack | 2D dp[i][w] = max(take, skip) |
| Edit distance | 2D dp[i][j] = ins/del/replace cost |
Recursion & Backtracking
Recursion rules in JavaScript
| Always define base case | Prevents infinite recursion |
| V8 stack depth | ~10k frames — convert deep recursion to iterative |
| Time = work × calls | fib naive = O(2ⁿ), memo = O(n) |
Backtracking template
function backtrack(path, options) {
if (isDone(path)) { result.push([...path]); return; }
for (const opt of options) {
if (!isValid(opt)) continue; // prune
path.push(opt); // choose
backtrack(path, nextOptions); // explore
path.pop(); // unchoose
}
}
Subsets — O(2ⁿ)
const res = [[]];
for (const n of nums)
res.push(...res.map(sub => [...sub, n]));
return res;
Monotonic Stack — O(n)
// Next greater element
const res = new Array(nums.length).fill(-1);
const stack = []; // stores indices, decreasing
for (let i=0; i<nums.length; i++) {
while (stack.length && nums[stack.at(-1)] < nums[i])
res[stack.pop()] = nums[i];
stack.push(i);
}
Fast & Slow Pointers — cycle detection
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true; // cycle!
}
JS Tips & Interview Strategy
JS DSA gotchas
| NaN !== NaN | Use Number.isNaN(x) to check |
| -0 === 0 | Object.is(-0, 0) → false |
| [10,9,2].sort() | "10","2","9" — must use (a,b)=>a-b |
| Integer overflow | MAX_SAFE_INT = 2⁵³-1 · use BigInt |
| mod 10⁹+7 | (a * b) % 1_000_000_007 |
Performance tips
| Avoid arr.shift() | O(n) — use pointer or Queue class |
| Map over {} | Any key type, no prototype pollution |
| [...arr] spread | O(n) copy — avoid in tight loops |
| Int32Array | Typed array — faster numeric ops |
Interview communication
| State complexity first | "This is O(n) time, O(1) space" |
| Brute → optimize | Show O(n²) then improve with HashMap |
| Edge cases | Empty input · single element · duplicates · negatives |
| Pattern recognition | Name the pattern: "sliding window", "two pointers" |
Useful JS utilities for DSA
Math.floor(n/2) // integer division
n >> 1 // same — bit shift (faster)
Math.max(...arr) // max of array
Array.from({length:n},(_,i)=>i) // [0..n-1]
str.split('').reverse().join('') // reverse str
Number.MAX_SAFE_INTEGER // 9007199254740991
Best Recommended Enterprise Stack 2026
Turborepo + pnpm
→
Next.js + shadcn/ui
→
NestJS + Prisma + Zod
→
PostgreSQL + pgvector + Redis
→
Vercel AI SDK + LiteLLM
→
Clerk + Stripe + Resend
→
Docker + K8s + GitOps
Monorepo — Turborepo + pnpm
Why monorepo
| Turborepo | build caching — only rebuild changed packages |
| pnpm workspaces | shared deps, symlinked node_modules |
| packages/shared | TS types, Zod schemas, utils — imported by all apps |
| turbo.json | pipeline config — order + caching rules |
Structure
apps/
web/ ← Next.js frontend
api/ ← NestJS backend
packages/
shared/ ← types, Zod schemas, utils
ui/ ← shadcn component library
config/ ← ESLint, tsconfig presets
Key commands
# scaffold
pnpm create turbo@latest
# run only affected + deps
turbo run build --filter=api...
# add dep to one workspace
pnpm --filter=api add @nestjs/core
Docker Multi-stage + Node 22
Why multi-stage
| Node 22 LTS | latest LTS (2024) — V8 11.8, native fetch, WebSocket |
| SWC compiler | 10-70× faster than tsc — used in NestJS + Next.js |
| alpine base | ~5 MB vs ~100 MB debian — smaller final image |
| .dockerignore | exclude node_modules, .git, dist to speed up build |
Production Dockerfile
# ── Stage 1: build ──
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ── Stage 2: run ──
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/main.js"]
Kubernetes + Helm + Terraform
AWS EKS deployment flow
| EKS | managed Kubernetes on AWS — control plane handled by AWS |
| Helm | K8s package manager — chart = versioned deployment template |
| Terraform | IaC multi-cloud (HCL) — provision VPC, EKS, RDS |
| AWS CDK | IaC in TypeScript — better DX for TypeScript teams |
Deploy with Helm
# install/upgrade release
helm upgrade --install api ./charts/api \
--set image.tag=v1.2.3 \
--namespace prod \
--create-namespace
Secrets & Config
| AWS Secrets Manager | store DB passwords, API keys — auto-rotate |
| HashiCorp Vault | self-hosted secrets + dynamic credentials |
| @nestjs/config | typed env with Zod schema validation on startup |
// validate env at startup with Zod
ConfigModule.forRoot({
validate: (env) => EnvSchema.parse(env)
})
Vercel AI SDK — Streaming & Tools
Core API
| streamText() | stream LLM response token-by-token to client |
| generateObject() | structured JSON output with Zod schema validation |
| tool() | define callable tools — the LLM decides when to call |
| useChat() | React hook — handles messages, streaming, loading state |
NestJS streaming example
import { streamText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await streamText({
model: openai('gpt-4o'),
system: 'You are a helpful assistant.',
messages,
tools: {
search: tool({
description: 'Search the knowledge base',
parameters: z.object({ query: z.string() }),
execute: async ({ query }) => await vectorSearch(query),
}),
},
});
return result.toDataStreamResponse();
RAG Pipeline — pgvector
RAG flow
| 1. Ingest | chunk docs → embed with OpenAI/Voyage → store in pgvector |
| 2. Query | embed user query → cosine similarity search → top-K chunks |
| 3. Generate | inject chunks into LLM prompt → get grounded answer |
| pgvector | PostgreSQL extension — vector(1536) column type |
pgvector setup
-- enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- store embeddings
ALTER TABLE docs ADD COLUMN
embedding vector(1536);
-- HNSW index for fast ANN search
CREATE INDEX ON docs USING hnsw
(embedding vector_cosine_ops);
-- semantic search
SELECT id, content,
1 - (embedding <=> '[...]'::vector) AS score
FROM docs
ORDER BY score DESC LIMIT 5;
LiteLLM — Multi-LLM Gateway
Why LiteLLM
| Unified API | OpenAI-compatible for all LLMs — swap models in config |
| Fallbacks | auto-switch to backup model if primary fails or rate-limits |
| Cost tracking | per-call cost + usage logging per user/team |
| No vendor lock-in | Claude → GPT-4o → Gemini without code changes |
Config + usage
# litellm config.yaml
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
- model_name: claude-3-5-sonnet
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
router_settings:
fallbacks:
- gpt-4o: [claude-3-5-sonnet]
num_retries: 3
LangChain.js / LangGraph
| LangChain.js | chains, prompts, memory, tool use — orchestration layer |
| LangGraph | stateful multi-step agents — nodes + edges + state machine |
OpenTelemetry Full Stack
Three pillars
| Traces | request flow across services — spans with start/end/tags |
| Metrics | Prometheus counters, histograms — visualized in Grafana |
| Logs | structured JSON — correlate with trace ID |
NestJS setup — init before bootstrap
// instrumentation.ts — run BEFORE NestJS
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from
'@opentelemetry/exporter-otlp-http';
import { getNodeAutoInstrumentations } from
'@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
AI observability
| LangSmith | LangChain-native — prompt tracing, run scores |
| LangFuse | open-source LLM obs — cost/call, latency, eval |
| Helicone | proxy-based — zero-code LLM logging |
Resilience Patterns
Circuit breaker — states
| CLOSED | normal — requests flow through |
| OPEN | failure threshold hit — fast-fail immediately |
| HALF_OPEN | probe: allow one request to test if service recovered |
Retry with backoff
import { retry, backoff, handleAll } from 'cockatiel';
const policy = retry(handleAll, {
maxAttempts: 3,
backoff: backoff.exponential({
initialDelay: 100,
maxDelay: 5000,
}),
});
Graceful shutdown (NestJS)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // listens for SIGTERM
await app.listen(3000);
}
// implement OnApplicationShutdown in services
async onApplicationShutdown(signal: string) {
await this.db.disconnect(); // drain connections
}
Security — AI + Enterprise
Prompt injection defense
function sanitizePrompt(input: string): string {
const banned = /ignore previous|jailbreak|system:/i;
if (banned.test(input))
throw new ForbiddenException('Blocked');
return input.slice(0, 2000); // length cap
}
Transport & API security
| mTLS | mutual TLS — both client + server present certificates |
| WAF | AWS WAF / Cloudflare — block OWASP Top 10 at edge |
| API key rotation | short-lived keys, store in Secrets Manager, never in code |
| CORS + Helmet | NestJS: app.use(helmet()) + CORS origin whitelist |
NestJS security setup
import helmet from 'helmet';
app.use(helmet());
app.enableCors({ origin: ['https://myapp.com'] });
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown fields
forbidNonWhitelisted: true,
}));
Auth — Clerk Recommended
Options
| Clerk | SaaS + orgs + billing — fastest to ship |
| Auth.js | flexible + self-hosted — any OAuth provider |
| Auth0 | enterprise compliance — SOC2, HIPAA |
| Supabase Auth | Postgres-native — great with Supabase stack |
Clerk in NestJS
import { clerkMiddleware } from '@clerk/express';
app.use(clerkMiddleware());
@Get('me')
getMe(@Req() req) {
return req.auth.userId; // Clerk userId
}
Stripe Webhooks in NestJS
Critical: verify signature
@Post('webhook')
@HttpCode(200)
async stripeWebhook(
@Req() req: RawBodyRequest<Request>,
@Headers('stripe-signature') sig: string,
) {
const event = this.stripe.webhooks
.constructEvent(req.rawBody, sig,
process.env.STRIPE_WEBHOOK_SECRET);
switch (event.type) {
case 'checkout.session.completed':
await this.provisionAccess(event.data);
break;
}
}
Key events
| checkout.session.completed | provision access after payment |
| customer.subscription.updated | sync plan/limits |
| payment_intent.payment_failed | notify + retry logic |
Outbox Pattern
Problem solved
Events lost if app crashes between DB write and message publish. The Outbox pattern atomically stores events in the same DB transaction.
Outbox table
CREATE TABLE outbox (
id UUID PRIMARY KEY,
event_type TEXT,
payload JSONB,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Atomic: domain change + event in ONE tx
BEGIN;
UPDATE orders SET status='paid' WHERE id=1;
INSERT INTO outbox (event_type, payload)
VALUES ('order.paid', '{"orderId":1}');
COMMIT;
Integration patterns
| Webhook verify | always check signature header |
| Idempotency | deduplicate by event ID — re-delivery is normal |
| Rate limit | exponential backoff when 429 from third-party |
PostHog + Resend
PostHog analytics
| Product analytics | events, funnels, retention — open-source |
| Feature flags | gradual rollouts without redeployment |
| Session replay | watch user interactions |
import posthog from 'posthog-js';
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY);
if (posthog.isFeatureEnabled('new-ui')) {
// show new UI
}
Resend — modern email
| Resend | developer-first — React Email templates |
| SendGrid / Postmark | battle-tested alternatives |
| Twilio | SMS + WhatsApp |
| Pusher / Ably | real-time WebSocket events |
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_KEY);
await resend.emails.send({
from: 'noreply@app.com',
to: user.email,
subject: 'Welcome!',
react: WelcomeEmail({ name: user.name }),
});
Prisma vs Drizzle ORM
Comparison
| Prisma | schema.prisma → auto-migrate → type-safe Prisma Client |
| Drizzle | TypeScript schema → SQL-like queries → lighter runtime |
| DX winner | Prisma — autocompletion, relations, Studio UI |
| Perf winner | Drizzle — no query engine, direct SQL driver |
| pgvector | both support it (Prisma: unsupported type workaround) |
Prisma quick reference
// find with relation
const user = await prisma.user.findUnique({
where: { id },
include: { posts: true },
});
// transaction
await prisma.$transaction([
prisma.order.create({ data: orderData }),
prisma.outbox.create({ data: eventData }),
]);
Redis — Multi-purpose Cache
Use cases
| Cache | GET/SET with TTL — avoid repeated DB queries |
| Sessions | store JWT or session data — fast lookup by token |
| Pub/Sub | real-time events between microservices |
| Rate limiting | INCR + EXPIRE per IP/user per minute |
| BullMQ queues | job queues — email, AI calls, report generation |
NestJS cache + BullMQ
// Cache manager
@Inject(CACHE_MANAGER) private cache: Cache;
await this.cache.set('key', data, 3600);
const cached = await this.cache.get('key');
// BullMQ processor
@Processor('emails')
export class EmailWorker {
@Process()
async handle(job: Job) {
await this.email.send(job.data);
}
}
Messaging — Kafka vs SQS vs Streams
When to use each
| Kafka | high-throughput event streaming, consumer groups, replay |
| Redis Streams | lightweight — same infra as your cache, ordered log |
| AWS SQS | managed, serverless-friendly, at-least-once delivery |
| AWS SNS | fan-out pub/sub — one message → many SQS subscribers |
NestJS transports
// Kafka microservice
const app = await NestFactory
.createMicroservice(AppModule, {
transport: Transport.KAFKA,
options: {
client: { brokers: ['kafka:9092'] },
},
});
// Emit event
this.client.emit('order.created', payload);
Rule: Redis Streams for <10k msg/s · Kafka for high-throughput replay · SQS for serverless/simple queues
Next.js App Router + React 19
Server vs Client Components
| Server Component | default — async, zero JS to client, direct DB/API access |
| 'use client' | opt-in for interactive components (useState, events) |
| Server Actions | async functions — mutations without API route |
| TanStack Query | client-side cache + background sync + optimistic UI |
Pattern: Server + Client split
// Server Component — runs on server
async function UserList() {
const users = await db.user.findMany();
return <ul>{users.map(u => <UserCard user={u}/>)}</ul>;
}
// Client Component — interactive
'use client';
export function LikeButton({ id }: { id: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(true)}>
{liked ? '❤️' : '🤍'}
</button>;
}
UI — Tailwind + shadcn/ui
Stack
| shadcn/ui | copy-paste components — you own the code, not a dep |
| Radix primitives | accessible headless base (Dialog, Popover, etc.) |
| Tailwind CSS | utility-first — no runtime CSS, JIT purging |
| cn() helper | merge Tailwind classes safely with clsx + twMerge |
Setup + usage
# add component (copies to components/ui/)
npx shadcn-ui@latest add button dialog
// cn() — safe class merging
import { cn } from '@/lib/utils';
export function Button({ className, ...props }) {
return <button
className={cn('px-4 py-2 rounded', className)}
{...props}
/>;
}
Vercel deployment
| Edge functions | run middleware at CDN edge — low latency globally |
| AI streaming | Vercel AI Gateway — caching + rate limiting for LLMs |
| Preview envs | auto-deploy every PR branch for QA |
AI Streaming in Next.js
useChat hook — Vercel AI SDK
'use client';
import { useChat } from 'ai/react';
export function Chat() {
const { messages, input, handleSubmit,
handleInputChange, isLoading } = useChat({
api: '/api/chat',
});
return (
<div>
{messages.map(m => (
<p key={m.id}>{m.role}: {m.content}</p>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange}/>
<button type='submit' disabled={isLoading}>
{isLoading ? '...' : 'Send'}
</button>
</form>
</div>
);
}
API route — streams from NestJS
// app/api/chat/route.ts
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4o'), messages,
});
return result.toDataStreamResponse();
}
GitHub Actions CI Pipeline
Monorepo CI with Turborepo
name: CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- run: pnpm install --frozen-lockfile
- run: pnpm turbo lint test build
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/org/api:\${{ github.sha }}
Key optimizations
| --filter=api... | only rebuild changed packages + deps |
| cache: turbo | remote Turbo cache — skip unchanged builds |
| concurrency | cancel in-progress runs on new push |
Argo CD — GitOps for K8s
GitOps principle
Git repo = single source of truth. Argo CD polls repo and reconciles K8s cluster state. git revert = instant rollback.
Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
source:
repoURL: https://github.com/org/infra
path: charts/api
targetRevision: main
destination:
namespace: production
syncPolicy:
automated:
prune: true # remove deleted resources
selfHeal: true # fix manual cluster changes
Progressive delivery
| Blue-green | run two identical envs, switch traffic instantly |
| Canary | route % of traffic to new version, watch metrics |
| Feature flags | Unleash / LaunchDarkly — decouple deploy from release |
AI-assisted Dev Workflow
Tools in 2026
| GitHub Copilot | inline completion + chat — integrated in VS Code/JetBrains |
| Cursor | AI-native editor — multi-file context, agentic edits |
| Claude Code | CLI agentic coding — full codebase context, terminal |
| Codium | auto-generate unit tests from code — CI-ready |
AI-powered PR review
# .github/workflows/ai-review.yml
- uses: coderabbitai/ai-pr-reviewer@v2
with:
openai_light_model: gpt-4o-mini
openai_heavy_model: gpt-4o
review_simple_changes: false
Best practices
| CLAUDE.md | project context file — guides Claude Code in repo |
| Review AI output | always verify generated code — security + correctness |
| Test generation | Codium / Copilot for boilerplate test scaffolding |
Multi-tenancy — RLS
Strategies
| Shared schema + tenantId | simplest — must filter every query |
| Schema-per-tenant | full isolation — migration complexity |
| DB-per-tenant | strongest isolation — highest cost |
| RLS (recommended) | Postgres enforces row filtering automatically |
PostgreSQL Row Level Security
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id =
current_setting('app.tenant_id')::uuid);
-- Set per request in NestJS middleware
await prisma.\$executeRaw
\`SET app.tenant_id = \${tenantId}\`;
Scaling AI Workloads
K8s patterns
| HPA | Horizontal Pod Autoscaler — scale on CPU or custom metrics |
| KEDA | scale on external signals: queue depth, Redis list length |
| GPU nodes | self-hosted LLMs (Llama, Mistral) on GPU node pool |
| Embedding cache | Redis TTL — avoid re-embedding same text repeatedly |
KEDA — scale on BullMQ
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
spec:
scaleTargetRef: { name: llm-worker }
triggers:
- type: redis
metadata:
listName: 'bull:llm-queue:wait'
listLength: '10' # 1 pod per 10 jobs
Serverless vs Kubernetes
Decision guide
| Vercel / Netlify | frontend + edge — zero ops, auto-scale, vendor managed |
| AWS Lambda | event-driven, low-traffic, stateless — pay per invocation |
| EKS (K8s) | long-running, stateful, AI workloads — full control |
| Hybrid | Next.js on Vercel + NestJS on EKS — best of both |
NestJS Lambda adapter
import { configure } from '@codegenie/serverless-express';
import { handler as expressHandler } from './app';
export const handler = configure({
app: expressHandler,
});
File storage
| AWS S3 | object storage — images, videos, exports |
| Presigned URLs | client uploads directly to S3 — skip your server |
| Cloudinary | image transformation + CDN on-the-fly |
| UploadThing | S3 abstraction — simple Next.js integration |
Core Architecture
Client-Server Model
# docker run nginx → internal flow:
Docker CLI ──REST API──▶ dockerd
↓
containerd
↓
runc (OCI)
┌────────┴────────┐
namespaces cgroups
(pid/net/mnt/uts) (CPU/mem/PID)
Linux Primitives
| pid namespace | own PID 1, cannot see host processes |
| net namespace | own eth0, IP, routing table |
| mnt namespace | own filesystem tree (pivot_root) |
| cgroups | CPU/memory/PID limits — kernel-enforced |
Image Layers (UnionFS / overlay2)
# Layers stack bottom → top (read-only):
Layer N ← COPY . . (your code)
Layer 3 ← RUN npm install (deps)
Layer 2 ← COPY package.json .
Layer 1 ← FROM node:18-alpine
+ Writable ← container adds at runtime (lost on rm)
Copy-on-Write: writes copy the file up to the writable layer. Shared layers = less disk. Cached layers = faster rebuilds.
Image Lifecycle
# build → tag → push → pull → run
docker build -t myapp:1.0 .
docker tag myapp:1.0 ghcr.io/user/myapp:1.0
docker push ghcr.io/user/myapp:1.0
docker pull ghcr.io/user/myapp:1.0
docker run -d -p 3000:3000 myapp:1.0
Dockerfile Mastery
Core Instructions
FROM node:18-alpine # base image
WORKDIR /app # set & create dir
COPY package*.json ./ # dep files FIRST
RUN npm ci --only=production # cached layer
COPY . . # source (changes often)
ENV NODE_ENV=production # baked into image
ARG BUILD_VER # build-time only, not in image
EXPOSE 3000 # docs only — not publish
ENTRYPOINT ["node"] # fixed executable
CMD ["dist/main.js"] # default args (overridable)
CMD vs ENTRYPOINT ★ interview classic
| ENTRYPOINT | fixed binary — not replaced by docker run args |
| CMD | default args — docker run img other.js overrides it |
| exec form | ["node","main.js"] — PID 1 = node, SIGTERM works ✓ |
| shell form | node main.js — PID 1 = /bin/sh, signals broken ✗ |
Layer Caching Strategy
# ❌ Bad — npm install reruns on every code change:
COPY . .
RUN npm ci
# ✅ Good — cached unless package.json changes:
COPY package*.json ./
RUN npm ci --only=production # stable cached layer
COPY . . # only this rebuilds
Multi-Stage Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
# 1GB builder → ~150MB runtime image
.dockerignore — always exclude: node_modules/ .git/ .env dist/ coverage/ *.log — prevents bloated build context.
Essential Commands
Build & Run
docker build -t myapp:1.0 .
docker build --no-cache -t myapp .
docker build --build-arg KEY=val .
docker build -f Dockerfile.prod .
docker run -d \
-p 3000:3000 \
-e DATABASE_URL=postgres://db:5432/mydb \
-v pgdata:/var/lib/postgresql/data \
--memory 512m --cpus 0.5 \
--rm --name api myapp:1.0
Inspect & Debug
docker ps # running
docker ps -a # all (incl stopped)
docker logs -f myapp # stream live logs
docker logs --tail 50 myapp
docker exec -it myapp sh # enter container
docker exec myapp env # print env vars
docker inspect myapp # full JSON metadata
docker inspect myapp \
--format='{{.State.ExitCode}}'
docker stats # live CPU/mem/net
Exit Codes
| 0 | success / clean exit |
| 1 | app error — check docker logs |
| 137 | OOMKilled — increase --memory |
| 143 | SIGTERM received (graceful stop) |
Cleanup
docker rm -f myapp # force remove running
docker rmi myapp:1.0
docker volume rm pgdata
docker system prune # stopped + dangling
docker system prune -a # + all unused images
docker system prune -a --volumes
docker system df # disk usage per type
Docker Compose
Full-Stack Example
version: '3.9'
services:
api:
build: .
ports: ['3000:3000']
environment:
DATABASE_URL: postgres://db:5432/mydb
REDIS_URL: redis://redis:6379
depends_on:
db: { condition: service_healthy }
networks: [app-net]
db:
image: postgres:16-alpine
volumes: [pgdata:/var/lib/postgresql/data]
environment: { POSTGRES_PASSWORD: secret }
healthcheck:
test: ['CMD-SHELL','pg_isready -U postgres']
interval: 5s
timeout: 3s
retries: 5
networks: [app-net]
redis:
image: redis:7-alpine
networks: [app-net]
volumes: { pgdata: }
networks: { app-net: }
Dev vs Prod Overrides
# override.yml (auto-loaded — dev only)
services:
api:
build: .
volumes: [./src:/app/src] # hot reload
environment: { NODE_ENV: development }
# prod.yml (explicit — production)
services:
api:
restart: always
environment: { NODE_ENV: production }
# docker-compose -f dc.yml -f dc.prod.yml up
Secrets: use .env + ${VAR} substitution. Never hardcode secrets in docker-compose.yml — it lives in git.
Networking & Volumes
Network Drivers
| bridge | default — custom bridge adds auto DNS by service name |
| host | shares host network stack — no port mapping, max perf |
| overlay | multi-host (Docker Swarm) |
| none | fully isolated — no networking |
DNS & Port Mapping
# Service name = hostname (custom networks only):
DATABASE_URL: postgres://db:5432/mydb # 'db' ✓
REDIS_URL: redis://redis:6379 # 'redis' ✓
# Port mapping:
docker run -p 3000:3000 myapp # host:container
docker run -p 8080:3000 myapp # remap
docker run -p 127.0.0.1:3000:3000 # localhost only
# EXPOSE = docs only. -p = actually publishes.
Volumes & Storage ★ interview classic
| Named volume | prod — Docker-managed, persists after rm |
| Bind mount | dev — host path → container, hot-reload |
| tmpfs | in-memory only, never on disk, ephemeral |
# Named volume (production):
docker run -v pgdata:/var/lib/postgresql/data postgres
# Bind mount (dev hot-reload):
docker run -v $(pwd)/src:/app/src myapp
# Backup a named volume:
docker run --rm \
-v pgdata:/src:ro -v $(pwd):/bak \
alpine tar czf /bak/pgdata.tar.gz -C /src .
Security & Hardening
Non-Root User (always in prod)
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node # node user pre-exists in alpine
EXPOSE 3000
CMD ["node", "dist/main.js"]
# Runtime hardening:
docker run \
--cap-drop ALL \
--read-only \
--security-opt no-new-privileges \
myapp
Minimal Base Images
| node:18 | ~1GB · full Debian · large attack surface |
| node:18-alpine | ~120MB · minimal OS · recommended |
| distroless/nodejs18 | ~90MB · no shell · most hardened |
Image Scanning
docker scout cves myapp:latest
trivy image --severity CRITICAL myapp:latest
trivy image --exit-code 1 myapp # fail CI on CVE
Resource Limits (cgroups)
docker run \
--memory 512m \ # OOMKill if exceeded
--memory-swap 512m \ # no swap
--cpus 0.5 \ # 50% of one core
--pids-limit 50 \ # prevent fork bombs
myapp
Pin base image by digest for reproducible builds: FROM node:18-alpine@sha256:abc123...
BuildKit & Advanced
Secret Mounts (never in layers)
# Dockerfile:
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
# Build command:
docker build --secret id=npm_token,src=.npmrc .
# SSH forwarding (private git repos):
RUN --mount=type=ssh git clone git@github.com:org/repo
docker build --ssh default .
# npm cache between builds:
RUN --mount=type=cache,target=/root/.npm \
npm ci
Zombie Process Fix
# Node as PID 1 ignores SIGTERM → docker stop hangs
# Fix 1: --init flag (auto-adds tini):
docker run --init myapp
# compose: add init: true to service
# Fix 2: Explicit tini in Dockerfile:
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/main.js"]
tini as PID 1: forwards SIGTERM to child (graceful shutdown) · reaps zombie processes · exits with child's exit code.
Sidecar Pattern
services:
app:
image: myapi
volumes: [logs:/var/log/app]
log-shipper: # ← sidecar
image: fluent/fluent-bit
volumes: [logs:/var/log/app:ro]
volumes: { logs: }
Swarm vs Kubernetes
| Docker Swarm | built-in, Compose syntax, simple — small teams |
| Kubernetes | HPA, GitOps, service mesh — enterprise scale |
CI/CD & Registry
GitHub Actions Pipeline
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=registry,ref=.../cache
cache-to: type=registry,ref=.../cache,mode=max
Image Tagging Strategy
| :latest | ❌ avoid in prod — mutable, no rollback possible |
| :git-sha | ✅ immutable, traceable, precise rollback |
| :v1.2.3 | ✅ semver for releases — human readable |
Cloud Deployment
| GCP Cloud Run | serverless, auto-scale to zero, simplest ops |
| AWS ECS Fargate | managed, no servers, AWS-native |
| AWS EKS / GKE | managed Kubernetes, enterprise scale |
| Railway / Render | push Dockerfile → URL — small projects |
Enable ECR tag immutability (IMMUTABLE) — prevents accidental tag overwrite in production.
Developer Learning Path
# Level 1: Containerize a NestJS app
# Level 2: Compose with PostgreSQL + Redis
# Level 3: Multi-stage builds → <200MB image
# Level 4: Deploy to AWS ECS/EKS + CI/CD
Interview Scenarios
Container vs VM
| Container | shared kernel · namespaces · ms startup · MBs |
| VM | full OS · hypervisor · min startup · GBs · stronger isolation |
Giant Image (2GB) Fix
# Diagnose:
docker history myapp:latest # find big layers
# Fix checklist:
# 1. node:18-alpine instead of node:18
# 2. .dockerignore: node_modules/ .git/ dist/
# 3. Multi-stage (devDeps stay in builder)
# 4. npm ci --only=production in runtime
# Result: 2GB → ~150MB
Secrets in Containers
# ❌ Never — visible in docker history:
ENV JWT_SECRET=supersecret
# ✅ Runtime — inject from external source:
docker run --env-file .env myapp # .env not in git
# ✅ Build-time — BuildKit secret mount:
RUN --mount=type=secret,id=token \
TOKEN=$(cat /run/secrets/token) npm ci
Debug: Container Starts Then Crashes
docker logs myapp # read the error
docker inspect myapp \
--format='{{.State.ExitCode}}' # 137 = OOMKilled
docker run -it \
--entrypoint sh myapp # poke around
docker run --memory 1g myapp # fix OOM
Full-stack compose: Next.js (frontend) + NestJS (backend) + PostgreSQL (db) + Redis (cache) — all managed by docker-compose.yml.
Rendering Strategies
When to Use Each
| SSR | per-request HTML — personalized, always fresh, dynamic |
| SSG | build-time HTML — ultra-fast, CDN-cached, no server cost |
| ISR | static + revalidate window — best of SSG + freshness |
| CSR | browser-only — dashboards, TanStack Query, user-specific |
| PPR ★ | static shell CDN instant + dynamic holes stream in (v15+) |
Code Patterns
// SSR — force fresh per request:
export const dynamic = 'force-dynamic'
async function Page() { const data = await fetch(url) }
// ISR — revalidate every hour:
const data = await fetch(url, { next: { revalidate: 3600 } })
// SSG + dynamic routes:
export async function generateStaticParams() {
return [{ id: '1' }, { id: '2' }]
}
// PPR (Next.js 15):
export const experimental_ppr = true
// Wrap dynamic parts in <Suspense> — rest is static
PPR = static shell from CDN instantly + dynamic islands stream in. Eliminates SSR vs SSG tradeoff entirely.
App Router Architecture
File Conventions
| layout.tsx | shared UI, persists across route changes |
| page.tsx | route content — makes route publicly accessible |
| loading.tsx | automatic Suspense fallback for the segment |
| error.tsx | error boundary — must be Client Component |
| not-found.tsx | 404 UI for the segment |
| route.ts | API route handler (GET, POST, PATCH, DELETE) |
Routing Patterns
app/
├── layout.tsx # root layout (wraps all)
├── page.tsx # /
├── blog/[slug]/page.tsx # /blog/:slug
├── docs/[...slug]/page.tsx # /docs/a/b/c (catch-all)
├── (auth)/login/page.tsx # /login (group, no URL segment)
├── (auth)/register/page.tsx # /register
├── @modal/page.tsx # parallel route slot
└── shop/(..)cart/page.tsx # intercepted route
Route groups (auth) share a layout without appearing in URL. Use for multi-layout apps (marketing vs dashboard).
Nested Layouts
// app/dashboard/layout.tsx
export default function DashboardLayout({
children
}: { children: React.ReactNode }) {
return (
<div>
<Sidebar /> // persists — never re-renders
<main>{children}</main>
</div>
)
}
Server vs Client Components
Decision Table ★ core concept
| Server (default) | async data, DB/ORM, no useState, no events, no browser API |
| 'use client' | useState/useEffect, onClick, browser APIs — only when needed |
Composition Pattern
// ❌ "use client" at top pollutes entire subtree:
'use client'
export default function Page() {
const data = await db.query() // ✗ can't do this
}
// ✅ Push 'use client' to leaf — tree stays server:
// page.tsx (server component):
async function Page() {
const data = await db.query() // ✓ server only
return <ClientButton data={data} /> // serialize → client
}
// ClientButton.tsx:
'use client'
export function ClientButton({ data }) {
const [open, setOpen] = useState(false)
...
}
// Pass server component as children — stays server ✓:
<ClientWrapper><ServerComponent /></ClientWrapper>
Boundary rule: 'use client' marks the boundary — not "runs on client only." Everything in the subtree below becomes a Client Component.
Data Fetching & Caching
Fetch Patterns in Server Components
// Cached (ISR — revalidate every hour):
const data = await fetch(url, {
next: { revalidate: 3600 }
})
// No cache (SSR — fresh per request):
const data = await fetch(url, { cache: 'no-store' })
// Tagged for on-demand revalidation:
const data = await fetch(url, {
next: { tags: ['posts'] }
})
// Parallel fetches — no waterfall:
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
])
4 Caching Layers ★ deep dive
| 1. Request Memoization | same fetch URL in one render → deduplicated (React) |
| 2. Data Cache | persistent across requests, server-side (Next.js) |
| 3. Full Route Cache | pre-rendered HTML+RSC payload on disk (Next.js) |
| 4. Router Cache | client-side prefetch cache in browser (Next.js) |
On-Demand Revalidation
// In Server Action or Route Handler:
revalidatePath('/blog') // purge all blog routes
revalidatePath('/blog/[slug]', 'page')
revalidateTag('posts') // purge tagged fetches
Server Actions & Mutations
Basic Server Action
// actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
// Validate with Zod:
const parsed = schema.safeParse({ title })
if (!parsed.success) return { error: parsed.error }
await db.post.create({ data: { title } })
revalidatePath('/blog')
redirect('/blog')
}
Form Action (progressive enhancement)
// page.tsx — works without JS!
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
useActionState + useOptimistic
'use client'
import { useActionState, useOptimistic } from 'react'
const [state, action, pending] = useActionState(
createPost, null
)
// pending = true while action runs
const [optimisticPosts, addOptimistic] = useOptimistic(posts)
async function handleAdd(formData: FormData) {
addOptimistic({ title: formData.get('title'), pending: true })
await createPost(formData) // rolls back on error
}
Server Actions replace API routes for mutations. No /api endpoint needed — call directly from components. Progressive enhancement built-in.
Streaming & Suspense
loading.tsx — Automatic Suspense
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton /> // shows instantly
}
// Wraps page.tsx in <Suspense> automatically
Granular Suspense (recommended)
import { Suspense } from 'react'
async function Page() {
return (
<div>
<StaticHeader /> // renders immediately
<Suspense fallback={<Skeleton />}>
<SlowUserProfile /> // streams when ready
</Suspense>
<Suspense fallback={<Skeleton />}>
<SlowFeed /> // streams in parallel
</Suspense>
</div>
)
}
// No waterfall — both slow components fetch in parallel
PPR — Partial Pre-Rendering
// next.config.ts:
experimental: { ppr: true }
// page.tsx:
export const experimental_ppr = true
async function Page() {
return (
<>
<StaticNav /> // CDN edge — instant
<Suspense fallback={<Spinner />}>
<DynamicFeed /> // streams after
</Suspense>
</>
)
}
// SSR waits for ALL data
// PPR: static shell NOW + dynamic streams IN
Performance
next/image — Core Web Vitals
import Image from 'next/image'
// Fixed size (width+height required):
<Image src="/hero.jpg" width={800} height={400}
alt="..." />
// Above-the-fold hero (priority = no lazy):
<Image src="/hero.jpg" fill priority
sizes="100vw" alt="..." />
// Responsive:
<Image src="/photo.jpg" fill
sizes="(max-width: 768px) 100vw, 50vw"
alt="..." />
// Auto: WebP/AVIF · lazy load · CLS=0 · blur placeholder
next/font — Zero Layout Shift
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
// Self-hosted — no external request, zero CLS ✓
Dynamic Imports + next/script
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), {
loading: () => <Skeleton />,
ssr: false, // client-only, skip SSR
})
const Modal = dynamic(() => import('./Modal')) // lazy load
| beforeInteractive | critical (consent) — blocks render |
| afterInteractive | analytics — loads after hydration |
| lazyOnload | chat widgets, ads — lowest priority |
Turbopack: next dev --turbo — 10× faster HMR (Rust-based bundler, stable in 2026 — replaces Webpack).
Middleware & API Routes
middleware.ts — Runs at Edge
import { NextRequest, NextResponse } from 'next/server'
export function middleware(req: NextRequest) {
const token = req.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(
new URL('/login', req.url)
)
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
// Uses: auth redirects · geo routing · A/B tests · CSP
Route Handlers (app/api)
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(req: NextRequest) {
const posts = await db.post.findMany()
return NextResponse.json(posts)
}
export async function POST(req: NextRequest) {
const body = await req.json()
const post = await db.post.create({ data: body })
return NextResponse.json(post, { status: 201 })
}
Route Handler vs Server Action
| Route Handler | webhooks, third-party callbacks, REST API for mobile |
| Server Action | in-app mutations, form submissions, no /api needed |
BFF pattern: Next.js server aggregates multiple microservice calls → one optimized response to client. Next.js → NestJS for complex domain logic.
State & Ecosystem
TanStack Query (server state)
'use client'
const { data, isPending } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
staleTime: 60_000,
})
const { mutate } = useMutation({
mutationFn: (data) => fetch('/api/posts', {
method: 'POST', body: JSON.stringify(data)
}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['posts'] }),
})
Zustand + URL State
// Zustand (global client state):
const useStore = create<State>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}))
// No Provider, no hydration mismatch ✓
// URL state (nuqs — shareable filters):
import { useQueryState } from 'nuqs'
const [search, setSearch] = useQueryState('q')
// /products?q=shoes — shareable, back-button works
Auth + AI + CMS
| Clerk | SaaS DX — currentUser() in RSC, middleware protect |
| Auth.js | OSS self-hosted — auth() in RSC, all providers |
| Vercel AI SDK | useChat/useCompletion — streaming AI Server Actions |
| CMS + ISR | Sanity/Payload webhook → revalidateTag('posts') |
React Compiler (stable 2026): auto-memoizes all components — no more useMemo/useCallback needed for performance.
Junior Developer Daily Essentials · JavaScript · React + Redux · SQL · Linux · NestJS · TypeScript · DSA · Enterprise Infra Architecture · Full-Stack Enterprise Tooling · Docker · Next.js · 2026