Backend
HTTP fundamentals, Node.js, NestJS, auth, and APIs.
Q. What are the common HTTP methods and what is each used for? easy ›
HTTP methods (verbs) describe the action a client wants to perform on a resource.
- GET — retrieve data; should never modify state (safe & idempotent).
- POST — create a new resource; not idempotent (calling twice creates two records).
- PUT — replace a resource entirely; idempotent.
- PATCH — partially update a resource.
- DELETE — remove a resource; idempotent.
- OPTIONS / HEAD — metadata; OPTIONS is used in CORS preflight, HEAD returns headers only.
Follow-up: “What does idempotent mean?” → Making the same request multiple times has the same effect as making it once.
Q. Map CRUD operations to HTTP methods. easy ›
CRUD is the set of four basic operations every persistent application needs. Each maps to an HTTP method:
| CRUD Operation | HTTP Method | Example |
|---|---|---|
| Create | POST | POST /users — create a new user |
| Read | GET | GET /users/1 — fetch user with id 1 |
| Update | PUT / PATCH | PUT /users/1 — replace user 1 |
| Delete | DELETE | DELETE /users/1 — remove user 1 |
PUT vs PATCH:
- PUT replaces the entire record. If you omit a field, it may be set to
null. - PATCH updates only the fields you send; everything else stays the same.
Tip: In interviews, mentioning the PUT vs PATCH distinction shows you understand REST semantics beyond the basics.
Q. Describe the structure of an HTTP request and response. easy ›
Both HTTP requests and responses share a similar three-part structure: a start line, headers, and an optional body.
HTTP Request:
POST /login HTTP/1.1
Host: api.example.com
Content-Type: application/json
{ "email": "user@example.com", "password": "s3cret" }
- Start line — method (
POST), path (/login), and HTTP version. - Headers — key-value metadata like
Content-Type,Authorization,Accept. - Body — the payload (JSON, form data, etc.). GET requests typically have no body.
HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json
{ "token": "eyJhbGciOi..." }
- Status line — HTTP version, status code (
200), and reason phrase (OK). - Headers —
Content-Type,Set-Cookie,Cache-Control, etc. - Body — the response payload (HTML, JSON, binary, etc.).
Remember: Headers are always plain text key-value pairs. The body is separated from the headers by a blank line.
Q. How can you transform a request or response before it reaches your handler? medium ›
Most backend frameworks give you hooks to modify requests and responses at different stages. In NestJS, the three main tools are:
- Middleware — runs first; has access to
req,res,next(). Great for logging, body parsing, and setting headers. - Pipes — transform or validate the incoming data before it reaches the handler. For example,
ParseIntPipeconverts a string param to a number. - Interceptors — wrap the handler call, so they can transform both the request and the response. They use RxJS observables.
NestJS Interceptor example — adding a timestamp to every response:
@Injectable()
export class TimestampInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => ({
...data,
timestamp: new Date().toISOString(),
})),
);
}
}
Apply it globally or per-controller:
@UseInterceptors(TimestampInterceptor)
@Controller('users')
export class UsersController { ... }
Key point: The NestJS request lifecycle order is: Middleware → Guards → Interceptors (before) → Pipes → Handler → Interceptors (after) → Exception Filters.
Q. What are HTTP status codes? Give the common ranges and examples. easy ›
HTTP status codes are three-digit numbers the server returns to tell the client what happened. They are grouped by the first digit:
- 1xx — Informational:
100 Continue,101 Switching Protocols. Rarely used directly. - 2xx — Success:
200 OK— standard success.201 Created— a new resource was created (common after POST).204 No Content— success but nothing to return (common after DELETE).
- 3xx — Redirection:
301 Moved Permanently— resource has a new URL.304 Not Modified— cached version is still valid.
- 4xx — Client Error:
400 Bad Request— malformed or invalid input.401 Unauthorized— authentication required or failed.403 Forbidden— authenticated but not allowed.404 Not Found— resource does not exist.422 Unprocessable Entity— validation error.
- 5xx — Server Error:
500 Internal Server Error— generic server failure.502 Bad Gateway— upstream server returned an invalid response.503 Service Unavailable— server is overloaded or under maintenance.
401 vs 403: 401 means “I don’t know who you are” (missing or invalid credentials). 403 means “I know who you are, but you’re not allowed to do this.” Getting this distinction right matters in interviews.
Q. How do you create and maintain a session on the backend? medium ›
There are two main approaches to maintaining user sessions: session-based (server-side) and token-based (client-side).
Session-Based Authentication:
- User logs in with credentials.
- Server creates a session object and stores it (in memory, Redis, or a database).
- Server sends back a session ID in a
Set-Cookieheader. - Browser automatically sends the cookie on every subsequent request.
- Server looks up the session ID to identify the user.
Token-Based Authentication (JWT):
- User logs in with credentials.
- Server creates and signs a JWT containing user claims.
- Client stores the token (localStorage or httpOnly cookie) and sends it via
Authorization: Bearer <token>. - Server verifies the token signature — no server-side lookup needed.
| Session Cookies | JWT | |
|---|---|---|
| State | Server-side (stateful) | Client-side (stateless) |
| Storage | Session store (Redis, DB) | Token stored by client |
| Scalability | Needs shared store across servers | Scales easily — no shared state |
| Revocation | Easy — delete the session | Hard — token valid until it expires |
| Size | Small cookie (just an ID) | Larger (carries payload) |
Best practice: For most web apps, use httpOnly, Secure, SameSite cookies for storing session identifiers or tokens. This mitigates XSS and CSRF risks compared to localStorage.
Q. What is a JWT and how is it used? medium ›
A JSON Web Token is a compact, signed token with three dot-separated parts: Header.Payload.Signature.
- Header — algorithm & token type.
- Payload — claims like
userId,role,exp(expiry). This is Base64-encoded, not encrypted — never put secrets here. - Signature — created with a secret key; lets the server verify the token wasn’t tampered with.
Flow: user logs in → server signs a JWT → client stores it and sends Authorization: Bearer <token> → server verifies the signature on each request.
Best practice: Use a short-lived access token + a long-lived refresh token. Store tokens in httpOnly cookies when possible to reduce XSS risk.
Q. What is middleware, and what is a guard? medium ›
Middleware is a function that runs in the request pipeline before the route handler, with access to req, res, and next(). Used for logging, body parsing, CORS, auth checks.
function logger(req, res, next) {
console.log(req.method, req.url);
next(); // pass control onward
}
Guards (NestJS) decide whether a request is allowed to proceed — they return true/false. They run after middleware and are the right place for authentication and role-based authorization.
@Injectable()
class AuthGuard {
canActivate(ctx) {
const req = ctx.switchToHttp().getRequest();
return Boolean(req.headers.authorization);
}
} Q. How do you handle errors and error responses in a backend API? medium ›
Good error handling keeps your API predictable and your users informed without leaking internals.
Key principles:
- Use a structured error shape — every error response should have a consistent format so clients can parse it reliably.
- Use typed/named exceptions — throw specific errors (
NotFoundException,ValidationError) rather than generic ones. - Never leak stack traces — in production, stack traces expose file paths and internal logic. Return a clean message and log the details server-side.
- Use the right status code — 400 for bad input, 401/403 for auth issues, 404 for missing resources, 500 for unexpected failures.
Express error-handling middleware:
app.use((err, req, res, next) => {
console.error(err.stack); // log full error server-side
res.status(err.status || 500).json({
error: {
message: err.message || 'Internal Server Error',
code: err.code || 'UNKNOWN_ERROR',
},
});
});
NestJS exception filters:
NestJS has a built-in exception layer. You can throw typed exceptions and they are automatically mapped to responses:
throw new NotFoundException('User not found');
// → { statusCode: 404, message: "User not found" }
For custom shapes, create an exception filter:
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception.getStatus();
response.status(status).json({
success: false,
error: exception.message,
timestamp: new Date().toISOString(),
});
}
}
Tip: Always validate input at the edge (with DTOs/pipes) so you can return a 400 before the error reaches your business logic.
Q. What is CORS and why does it block requests? medium ›
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different origin (domain, protocol, or port) than the one that served the page.
Why does it exist?
Without CORS, a malicious site could make requests to your bank’s API using your cookies — the browser would send them automatically. CORS prevents this by requiring the server to explicitly allow cross-origin access.
How it works:
- The browser sends a request from
http://frontend.comtohttp://api.example.com. - If the origins differ, the browser checks for CORS headers in the response.
- For “non-simple” requests (PUT, DELETE, custom headers, JSON content-type), the browser first sends an OPTIONS preflight request asking “is this allowed?”
- The server responds with headers like
Access-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headers. - If the headers permit it, the browser proceeds with the actual request. Otherwise, it blocks it.
Key CORS headers:
Access-Control-Allow-Origin— which origins are allowed (*for any, or a specific origin).Access-Control-Allow-Methods— which HTTP methods are permitted.Access-Control-Allow-Headers— which custom headers the client can send.Access-Control-Allow-Credentials— whether cookies/auth headers are allowed (cannot use*for origin when this istrue).
Fix on the server (Express example):
const cors = require('cors');
app.use(cors({
origin: 'https://frontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
}));
Remember: CORS is enforced by the browser, not the server. Server-to-server requests (e.g., from your backend to another API) are never blocked by CORS. If you see a CORS error, the fix is always on the server, not the client.
Q. Is Node.js single-threaded? Explain the event loop. medium ›
Yes and no. Node.js runs your JavaScript on a single thread, but it delegates I/O operations (file reads, network calls, DNS lookups) to libuv, which maintains a thread pool (default 4 threads).
The Event Loop is the mechanism that coordinates this:
- Timers — execute callbacks from
setTimeoutandsetInterval. - Pending callbacks — I/O callbacks deferred from the previous cycle.
- Poll — retrieve new I/O events; execute I/O-related callbacks. This is where Node spends most of its time.
- Check — execute
setImmediatecallbacks. - Close callbacks — e.g.,
socket.on('close', ...).
Between each phase, Node processes the microtask queue (resolved Promises, process.nextTick).
Why does this matter?
- I/O is non-blocking — while one request waits for a database response, Node can handle other requests.
- CPU-heavy work blocks the loop — a big
forloop or image processing will freeze all other requests because there is only one JS thread.
How to handle CPU-heavy tasks:
- Use Worker Threads (
worker_threadsmodule) to run code on a separate thread. - Offload to a child process (
child_process.fork). - Use a job queue (Bull, BeeQueue) for background processing.
Key takeaway: Node.js is single-threaded for JavaScript execution but uses multiple threads under the hood for I/O. The event loop is what makes non-blocking async work possible.
Q. Callbacks vs Promises vs async/await — what's the difference? easy ›
All three are ways to handle asynchronous operations in JavaScript. Each is an evolution of the previous.
Callbacks — a function passed as an argument, called when the async work completes. Works, but nesting multiple callbacks creates callback hell (deeply indented, hard-to-read code).
fs.readFile('a.txt', (err, data) => {
if (err) throw err;
fs.readFile('b.txt', (err, data2) => {
if (err) throw err;
console.log(data, data2); // nested and messy
});
});
Promises — an object representing a future value. Chain .then() for success and .catch() for errors. Flattens the nesting.
readFilePromise('a.txt')
.then(data => readFilePromise('b.txt'))
.then(data2 => console.log(data2))
.catch(err => console.error(err));
async/await — syntactic sugar over Promises. Makes async code look and behave like synchronous code. Uses try/catch for error handling.
async function readFiles() {
try {
const data = await readFilePromise('a.txt');
const data2 = await readFilePromise('b.txt');
console.log(data, data2);
} catch (err) {
console.error(err);
}
}
Tip:
async/awaitis still using Promises under the hood — it’s not a different mechanism. Everyasyncfunction returns a Promise, andawaitpauses execution until that Promise resolves.
Q. What is the difference between process.nextTick, microtasks, and setTimeout? hard ›
These three schedule callbacks at different points in the Node.js event loop, and their execution order is a common interview question.
Execution priority (highest to lowest):
process.nextTick— fires immediately after the current operation, before any other I/O or timer. It is processed from the nextTick queue, which is drained completely before moving on.- Microtasks (Promise
.then,queueMicrotask) — processed from the microtask queue, right after the nextTick queue is empty. These run between every phase of the event loop. setTimeout(fn, 0)/setImmediate— these are macrotasks.setTimeoutruns in the Timers phase;setImmediateruns in the Check phase.
Example:
console.log('1 - start');
setTimeout(() => console.log('2 - setTimeout'), 0);
Promise.resolve().then(() => console.log('3 - Promise'));
process.nextTick(() => console.log('4 - nextTick'));
console.log('5 - end');
Output:
1 - start
5 - end
4 - nextTick
3 - Promise
2 - setTimeout
Synchronous code runs first (1, 5), then process.nextTick (4), then the microtask/Promise (3), and finally the macrotask/setTimeout (2).
Warning: Overusing
process.nextTickcan starve I/O because the nextTick queue is drained completely before the event loop continues. PrefersetImmediateorqueueMicrotaskunless you specifically need nextTick’s priority.
Q. What does module.exports vs require do, and how does ESM differ? easy ›
Node.js has two module systems: CommonJS (CJS) and ES Modules (ESM).
CommonJS (the original Node.js way):
module.exports— defines what a file exports.require()— imports a module synchronously.
// math.js
module.exports = { add: (a, b) => a + b };
// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5
ES Modules (the modern standard):
export/export default— defines exports.import— imports a module. Statically analyzed and asynchronous.
// math.mjs
export const add = (a, b) => a + b;
// app.mjs
import { add } from './math.mjs';
console.log(add(2, 3)); // 5
Key differences:
| CommonJS | ES Modules | |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous | Asynchronous |
| Parsing | Dynamic (can require conditionally) | Static (imports hoisted to top) |
| Tree-shaking | Not possible | Supported (bundlers remove unused exports) |
| File extension | .js (default) | .mjs or .js with "type": "module" in package.json |
Tip: To use ESM in Node.js, either name your files
.mjsor add"type": "module"to yourpackage.json. Most modern frameworks (including NestJS and Astro) use ESM by default.
Q. Explain the core building blocks of NestJS. medium ›
NestJS organizes applications around a set of core building blocks, each with a specific responsibility:
- Module (
@Module) — a class that groups related controllers and providers. Every app has at least a rootAppModule. Modules can import other modules. - Controller (
@Controller) — handles incoming HTTP requests and returns responses. Defines routes with decorators like@Get(),@Post(), etc. - Provider / Service (
@Injectable) — contains business logic. Services are injected into controllers via dependency injection. - Pipe — transforms or validates input data before it reaches the handler. Example:
ValidationPipevalidates DTOs,ParseIntPipeconverts strings to numbers. - Guard (
@Injectable, implementsCanActivate) — determines whether a request is allowed to proceed. Used for authentication and authorization. - Interceptor (
@Injectable, implementsNestInterceptor) — wraps the handler execution. Can transform the response, add logging, or cache results. - Exception Filter (
@Catch) — catches thrown exceptions and converts them into HTTP responses.
Request lifecycle order:
Incoming Request
→ Middleware
→ Guards
→ Interceptors (before)
→ Pipes
→ Route Handler
→ Interceptors (after)
→ Exception Filters (if error)
→ Response
Key insight: Each building block has a single responsibility. Guards only decide yes/no, Pipes only validate/transform data, Interceptors wrap the handler. This separation makes NestJS apps testable and maintainable.
Q. What is Dependency Injection and why does NestJS use it? medium ›
Dependency Injection (DI) is a design pattern where a class receives its dependencies from the outside rather than creating them itself. An IoC (Inversion of Control) container manages the creation and lifetime of these dependencies.
Without DI (tightly coupled):
class UserController {
private userService = new UserService(); // creates its own dependency
}
With DI (loosely coupled):
@Controller('users')
class UserController {
constructor(private readonly userService: UserService) {}
// NestJS injects UserService automatically
}
Why NestJS uses DI:
- Loose coupling — classes depend on abstractions, not concrete implementations. You can swap a
DatabaseServicefor aMockDatabaseServicewithout changing the controller. - Testability — in unit tests, you inject mock or stub services instead of real ones.
- Single responsibility — each service handles one concern; the container wires them together.
- Lifecycle management — the container controls whether a service is a singleton (default in NestJS), request-scoped, or transient.
How it works in NestJS:
- Mark a class as injectable with
@Injectable(). - Register it in a module’s
providersarray. - Declare it as a constructor parameter wherever you need it.
@Injectable()
export class UserService {
findAll() { return ['Alice', 'Bob']; }
}
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
Tip: If an interviewer asks “what is IoC?” — it is the principle that the framework controls object creation and lifecycle, not your code. DI is the most common way to implement IoC.
Q. What is a DTO and why use class-validator? easy ›
A DTO (Data Transfer Object) is a plain class that defines the shape of data coming into or going out of your API. It does not contain business logic — it is purely a data contract.
Why use DTOs?
- Validation — ensure incoming data matches the expected shape before it reaches your service.
- Documentation — the DTO clearly shows what fields an endpoint expects.
- Type safety — TypeScript catches mismatches at compile time.
class-validator is a library that uses decorators to define validation rules on DTO properties. Paired with NestJS’s ValidationPipe, invalid requests are automatically rejected with a 400 error.
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
Using it in a controller:
@Post()
create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
Enable ValidationPipe globally in main.ts:
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // strip unknown properties
forbidNonWhitelisted: true, // throw if unknown properties are sent
transform: true, // auto-transform payloads to DTO instances
}));
Tip: Always enable
whitelist: trueto strip unexpected fields. This prevents users from injecting extra properties (likeisAdmin: true) into your database.
Q. What makes an API RESTful? medium ›
REST (Representational State Transfer) is an architectural style for designing networked APIs. An API is considered RESTful when it follows these principles:
- Stateless — every request contains all the information the server needs. The server does not store client session state between requests.
- Resource-based URLs — endpoints represent resources (nouns), not actions (verbs). Use
/users/5instead of/getUser?id=5. - Correct HTTP methods — use GET to read, POST to create, PUT/PATCH to update, DELETE to remove.
- Proper status codes — return 201 for creation, 404 for not found, 400 for bad input, etc.
- Uniform interface — consistent URL patterns, predictable request/response formats (usually JSON).
- Client-server separation — the client and server are independent; the client only knows the API contract.
Good REST design:
GET /users → list all users
GET /users/42 → get user 42
POST /users → create a new user
PUT /users/42 → replace user 42
PATCH /users/42 → partially update user 42
DELETE /users/42 → delete user 42
GET /users/42/posts → list posts by user 42
Common mistakes:
- Using verbs in URLs:
/getUsers,/deleteUser/42 - Using POST for everything
- Returning 200 for errors with an error message in the body
- Not using plural nouns for collections (
/uservs/users)
Interview tip: REST is a set of guidelines, not a strict specification. In practice, very few APIs are “purely” RESTful — what matters is consistent, predictable conventions that your team agrees on.
Q. REST vs GraphQL — when would you choose each? medium ›
REST and GraphQL are two approaches to building APIs. Each has trade-offs.
REST:
- Multiple endpoints, one per resource (
/users,/posts,/comments). - The server decides what data to return for each endpoint.
- Simple HTTP caching with
Cache-Control,ETag, etc. - Can lead to over-fetching (getting more fields than you need) or under-fetching (needing multiple requests to assemble a view).
GraphQL:
- A single endpoint (typically
/graphql). - The client sends a query specifying exactly which fields it needs.
- No over-fetching or under-fetching — the response shape matches the query.
- Built-in schema and type system for self-documentation.
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many (one per resource) | Single (/graphql) |
| Data shape | Server decides | Client decides |
| Caching | Easy (HTTP-level) | Harder (needs client-side cache like Apollo) |
| Learning curve | Lower | Higher (schema, resolvers, query language) |
| File uploads | Straightforward | Requires extra setup |
| Real-time | Polling or WebSockets | Subscriptions built in |
When to choose REST:
- Simple CRUD applications with predictable data needs.
- When HTTP caching is important.
- When the team is more familiar with REST patterns.
When to choose GraphQL:
- Complex frontends that need different slices of data on different screens.
- Mobile apps where minimizing payload size matters.
- When you have many related resources and want to avoid multiple round trips.
Tip: They are not mutually exclusive. Some teams use REST for simple CRUD and add a GraphQL layer for complex, relationship-heavy queries.
Q. How do you secure a backend application? (name several) hard ›
Backend security is not a single feature — it is a layered approach. Here are the essential practices:
Authentication & Passwords:
- Hash passwords with bcrypt (or Argon2) — never store plain text. bcrypt includes a salt and is intentionally slow to resist brute-force attacks.
- Use JWT or session cookies for auth. Prefer httpOnly, Secure, SameSite cookies to reduce XSS and CSRF risk.
Input Validation:
- Validate and sanitize all input — never trust data from the client. Use a validation library (class-validator, Joi, Zod) at the API boundary.
- Parameterized queries (prepared statements) — prevent SQL injection. Never concatenate user input into SQL strings.
// BAD — SQL injection risk
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// GOOD — parameterized query
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
Transport & Headers:
- HTTPS everywhere — encrypt data in transit. Redirect HTTP to HTTPS.
- Use helmet (Express middleware) — sets security headers like
X-Content-Type-Options,Strict-Transport-Security,X-Frame-Options. - CORS configuration — only allow trusted origins.
Rate Limiting & Abuse Prevention:
- Rate-limit endpoints — prevent brute-force login attempts and DDoS. Use libraries like
express-rate-limitor@nestjs/throttler. - Limit payload size — prevent large request body attacks.
Other Important Measures:
- Principle of least privilege — give services and database users only the permissions they need.
- Keep dependencies updated — run
npm auditregularly to catch known vulnerabilities. - Never expose stack traces in production — return generic error messages and log details server-side.
- Environment variables — store secrets (API keys, DB passwords) in environment variables, never in code.
Remember: Security is defense in depth. No single measure is enough. Combining multiple layers — hashing, validation, HTTPS, rate-limiting, least privilege — makes your application resilient against different attack vectors.