Practice Problems — Node.js · SQL · GraphQL
Interview Relevance: Critical — These problems mirror real take-home tests and technical screens. Work through each one without looking at the solution first. Time yourself: aim for 15–20 min per problem.
How to Use This Guide
Part 1 — Node.js Backend Logic
Problem 1.1 — Rate Limiter Middleware
Prompt: Write an Express middleware that limits each IP address to 10 requests per minute. Requests beyond the limit should return HTTP 429.
Concepts tested: Closures, Maps, timers, middleware pattern.
Solution:
// rate-limiter.js
function rateLimiter(maxRequests = 10, windowMs = 60_000) {
// Map<ip, { count, resetAt }>
const store = new Map();
return function (req, res, next) {
const ip = req.ip;
const now = Date.now();
// Get or create window entry for this IP
let entry = store.get(ip);
if (!entry || now > entry.resetAt) {
// First request or window has expired — start fresh
entry = { count: 1, resetAt: now + windowMs };
store.set(ip, entry);
return next();
}
entry.count++;
if (entry.count > maxRequests) {
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
res.set("Retry-After", retryAfter);
return res.status(429).json({
error: "Too Many Requests",
message: `Limit: ${maxRequests} req/min. Retry after ${retryAfter}s.`,
});
}
next();
};
}
// Usage
import express from "express";
const app = express();
app.use(rateLimiter(10, 60_000)); // 10 req per 60 seconds
app.get("/api/data", (req, res) => {
res.json({ message: "OK" });
});
app.listen(3000);Key insight: The window is fixed (resets at a fixed interval). A sliding window is more accurate but requires storing timestamps per request. In interviews, mention both and explain the trade-off.
Problem 1.2 — Async Queue with Concurrency Limit
Prompt: Implement an async task queue that processes tasks with a concurrency limit of N (max N tasks running simultaneously). Tasks beyond the limit must wait in a queue.
Concepts tested: Promises, async/await, queues, concurrency control.
Solution:
// async-queue.js
class AsyncQueue {
constructor(concurrency = 2) {
this.concurrency = concurrency;
this.running = 0; // currently executing tasks
this.queue = []; // pending tasks
}
// Add a task (function that returns a Promise) to the queue
add(task) {
return new Promise((resolve, reject) => {
// Wrap task with resolve/reject so caller can await it
this.queue.push({ task, resolve, reject });
this._run();
});
}
_run() {
// Drain the queue while slots are available
while (this.running < this.concurrency && this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this._run(); // slot freed — try to start the next task
});
}
}
}
// ── Test ────────────────────────────────────────────────────────────────────
const queue = new AsyncQueue(2); // max 2 concurrent tasks
const delay = (ms, label) =>
new Promise((res) =>
setTimeout(() => {
console.log(`✅ ${label} done after ${ms}ms`);
res(label);
}, ms)
);
// Add 5 tasks — only 2 run at a time
queue.add(() => delay(1000, "A"));
queue.add(() => delay(500, "B"));
queue.add(() => delay(800, "C")); // waits for A or B
queue.add(() => delay(200, "D")); // waits for A or B
queue.add(() => delay(600, "E")); // waits for a slot
// Output order: B(500ms), A(1000ms), D(200ms after B slot freed)...Problem 1.3 — JWT Authentication Middleware
Prompt: Write Express middleware that verifies a JWT from the Authorization: Bearer <token> header and attaches the decoded payload to req.user. Return 401 if missing or invalid.
Concepts tested: JWT, middleware chaining, error handling.
Solution:
// auth.middleware.js
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret-change-in-prod";
export function authenticate(req, res, next) {
const authHeader = req.headers["authorization"];
// 1. Header must exist and start with "Bearer "
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing or malformed token" });
}
const token = authHeader.slice(7); // remove "Bearer "
try {
// 2. Verify signature + expiry
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload; // { id, email, role, iat, exp }
next();
} catch (err) {
const message =
err.name === "TokenExpiredError" ? "Token expired" : "Invalid token";
return res.status(401).json({ error: message });
}
}
// Usage
import express from "express";
const app = express();
app.use(express.json());
// Public route
app.post("/login", (req, res) => {
const { email, password } = req.body;
// ... validate credentials ...
const token = jwt.sign({ id: 1, email, role: "user" }, JWT_SECRET, {
expiresIn: "1h",
});
res.json({ token });
});
// Protected route — middleware runs first
app.get("/profile", authenticate, (req, res) => {
res.json({ user: req.user }); // req.user guaranteed to exist here
});Problem 1.4 — Debounce & Throttle from Scratch
Prompt: Implement debounce(fn, delay) and throttle(fn, limit) without using lodash.
Concepts tested: Closures, timers, higher-order functions.
Solution:
// ── Debounce: fires AFTER silence for `delay` ms ──────────────────────────
function debounce(fn, delay) {
let timerId = null;
return function (...args) {
clearTimeout(timerId); // cancel previous timer
timerId = setTimeout(() => {
fn.apply(this, args); // fire with original context + args
}, delay);
};
}
// ── Throttle: fires AT MOST once per `limit` ms ───────────────────────────
function throttle(fn, limit) {
let lastFired = 0;
return function (...args) {
const now = Date.now();
if (now - lastFired >= limit) {
lastFired = now;
fn.apply(this, args);
}
};
}
// ── Test ──────────────────────────────────────────────────────────────────
const log = (msg) => console.log(`[${Date.now()}] ${msg}`);
const debouncedLog = debounce(log, 300);
debouncedLog("A"); // timer starts
debouncedLog("B"); // resets timer
debouncedLog("C"); // resets timer → "C" fires after 300ms
const throttledLog = throttle(log, 1000);
throttledLog("X"); // fires immediately
setTimeout(() => throttledLog("Y"), 500); // ignored (< 1000ms)
setTimeout(() => throttledLog("Z"), 1200); // fires (> 1000ms)When to use which:
- Debounce — search-as-you-type, window resize, form autosave (fire after user stops)
- Throttle — scroll handlers, mouse tracking, API rate limiting (fire periodically)
Part 2 — SQL Schema Design & Queries
Problem 2.1 — E-Commerce Schema Design
Prompt: Design a relational schema for an e-commerce platform. Support: users, products, orders, order items, and product categories.
Schema Diagram:
SQL DDL:
-- Users
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Categories (self-referencing for subcategories)
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
parent_id INT REFERENCES categories(id) ON DELETE SET NULL
);
-- Products
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock_qty INT NOT NULL DEFAULT 0 CHECK (stock_qty >= 0),
category_id INT REFERENCES categories(id) ON DELETE SET NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Orders
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','processing','shipped','delivered','cancelled')),
total_amount DECIMAL(10, 2) NOT NULL,
ordered_at TIMESTAMP DEFAULT NOW()
);
-- Order Items (junction table — one row per product in an order)
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id INT NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10, 2) NOT NULL -- snapshot of price at time of order
);
-- ── Indexes for common queries ─────────────────────────────────────────────
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_price ON products(price);Key design decisions:
unit_priceinorder_itemsis a snapshot — if product price changes later, historical orders remain accurate.parent_idincategoriesenables nested categories (Electronics → Phones → Android).ON DELETE RESTRICTonorder_items.product_idprevents deleting products that appear in orders.
Problem 2.2 — Complex SQL Queries
Using the schema above, write queries for common interview questions.
Query 1: Top 5 best-selling products (by total quantity sold)
SELECT
p.id,
p.name,
SUM(oi.quantity) AS total_sold,
SUM(oi.quantity * oi.unit_price) AS total_revenue
FROM products p
JOIN order_items oi ON oi.product_id = p.id
JOIN orders o ON o.id = oi.order_id
WHERE o.status = 'delivered'
GROUP BY p.id, p.name
ORDER BY total_sold DESC
LIMIT 5;Query 2: Users who have spent more than $1,000 in the last 30 days
SELECT
u.id,
u.name,
u.email,
SUM(o.total_amount) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE
o.status IN ('shipped', 'delivered')
AND o.ordered_at >= NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name, u.email
HAVING SUM(o.total_amount) > 1000
ORDER BY total_spent DESC;Query 3: Products that have NEVER been ordered
-- Using LEFT JOIN (preferred — usually faster)
SELECT p.id, p.name, p.price
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
WHERE oi.id IS NULL;
-- Alternative using NOT EXISTS
SELECT p.id, p.name, p.price
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);Query 4: Monthly revenue trend (last 6 months)
SELECT
DATE_TRUNC('month', o.ordered_at) AS month,
COUNT(DISTINCT o.id) AS total_orders,
COUNT(DISTINCT o.user_id) AS unique_customers,
SUM(o.total_amount) AS revenue,
ROUND(AVG(o.total_amount), 2) AS avg_order_value
FROM orders o
WHERE
o.status = 'delivered'
AND o.ordered_at >= NOW() - INTERVAL '6 months'
GROUP BY 1
ORDER BY 1 DESC;Query 5: Running total (window function)
-- Cumulative revenue per user, ordered by date
SELECT
u.name,
o.ordered_at::DATE AS order_date,
o.total_amount,
SUM(o.total_amount) OVER (
PARTITION BY o.user_id
ORDER BY o.ordered_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'delivered'
ORDER BY u.name, o.ordered_at;Problem 2.3 — Blog Platform Schema + Query Challenge
Prompt: Design a schema for a blog platform with users, posts, tags, and comments. Then write a query to find the top 3 authors by engagement (likes + comments) in the last 7 days.
DDL:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
author_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
status VARCHAR(10) DEFAULT 'draft' CHECK (status IN ('draft','published')),
published_at TIMESTAMP
);
CREATE TABLE tags (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE post_tags (
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
tag_id INT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id) -- composite PK prevents duplicate tags on a post
);
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
author_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE likes (
id SERIAL PRIMARY KEY,
post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE (post_id, user_id) -- a user can only like a post once
);Query — Top 3 Authors by Engagement (last 7 days):
SELECT
u.id,
u.username,
COUNT(DISTINCT l.id) AS likes_received,
COUNT(DISTINCT c.id) AS comments_received,
COUNT(DISTINCT l.id) + COUNT(DISTINCT c.id) AS total_engagement
FROM users u
JOIN posts p ON p.author_id = u.id
AND p.status = 'published'
AND p.published_at >= NOW() - INTERVAL '7 days'
LEFT JOIN likes l ON l.post_id = p.id
AND l.created_at >= NOW() - INTERVAL '7 days'
LEFT JOIN comments c ON c.post_id = p.id
AND c.created_at >= NOW() - INTERVAL '7 days'
GROUP BY u.id, u.username
ORDER BY total_engagement DESC
LIMIT 3;Part 3 — GraphQL Schema & Resolvers
Problem 3.1 — Design a GraphQL API for the Blog Platform
Prompt: Using the blog schema above, design a complete GraphQL schema and write the key resolvers.
Schema:
# ── Scalar types ─────────────────────────────────────────────────────────────
scalar DateTime
# ── Types ─────────────────────────────────────────────────────────────────────
type User {
id: ID!
username: String!
email: String!
posts: [Post!]!
postCount: Int!
}
type Post {
id: ID!
title: String!
body: String!
status: PostStatus!
publishedAt: DateTime
author: User!
tags: [Tag!]!
comments: [Comment!]!
likes: Int! # count only — privacy-friendly
isLikedBy(userId: ID!): Boolean!
}
type Comment {
id: ID!
body: String!
createdAt: DateTime!
author: User!
post: Post!
}
type Tag {
id: ID!
name: String!
posts: [Post!]!
}
enum PostStatus {
DRAFT
PUBLISHED
}
# ── Input types (for mutations) ───────────────────────────────────────────────
input CreatePostInput {
title: String!
body: String!
tagIds: [ID!]
}
input UpdatePostInput {
title: String
body: String
status: PostStatus
tagIds: [ID!]
}
# ── Pagination input ──────────────────────────────────────────────────────────
input PaginationInput {
limit: Int = 10
offset: Int = 0
}
# ── Root types ────────────────────────────────────────────────────────────────
type Query {
# Users
user(id: ID!): User
users: [User!]!
# Posts
post(id: ID!): Post
posts(status: PostStatus, tagId: ID, page: PaginationInput): [Post!]!
# Tags
tags: [Tag!]!
tag(id: ID!): Tag
# Search
searchPosts(query: String!): [Post!]!
}
type Mutation {
# Auth (simplified)
createUser(username: String!, email: String!): User!
# Posts
createPost(authorId: ID!, input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
publishPost(id: ID!): Post!
deletePost(id: ID!): Boolean!
# Comments
addComment(postId: ID!, authorId: ID!, body: String!): Comment!
deleteComment(id: ID!): Boolean!
# Likes
likePost(postId: ID!, userId: ID!): Post!
unlikePost(postId: ID!, userId: ID!): Post!
# Tags
createTag(name: String!): Tag!
}
type Subscription {
newComment(postId: ID!): Comment!
newLike(postId: ID!): Post!
}Resolver Execution Flow:
Resolvers:
// resolvers.js
import DataLoader from "dataloader";
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();
// ── DataLoaders (created per-request in context) ───────────────────────────
export function createLoaders(db) {
return {
// Batch-load users by ID array → single query
user: new DataLoader(async (ids) => {
const users = await db.query(`SELECT * FROM users WHERE id = ANY($1)`, [
ids,
]);
return ids.map((id) => users.find((u) => u.id === Number(id)) ?? null);
}),
// Batch-load tags by post ID array
postTags: new DataLoader(async (postIds) => {
const rows = await db.query(
`SELECT pt.post_id, t.*
FROM tags t
JOIN post_tags pt ON pt.tag_id = t.id
WHERE pt.post_id = ANY($1)`,
[postIds]
);
// Group by post_id
return postIds.map((id) => rows.filter((r) => r.post_id === Number(id)));
}),
// Batch-load comment counts
likesCount: new DataLoader(async (postIds) => {
const rows = await db.query(
`SELECT post_id, COUNT(*) AS count
FROM likes
WHERE post_id = ANY($1)
GROUP BY post_id`,
[postIds]
);
return postIds.map((id) =>
Number(rows.find((r) => r.post_id === Number(id))?.count ?? 0)
);
}),
};
}
// ── Resolvers ─────────────────────────────────────────────────────────────────
export const resolvers = {
Query: {
user: async (_, { id }, { db }) => {
const [user] = await db.query("SELECT * FROM users WHERE id=$1", [id]);
return user ?? null;
},
users: async (_, __, { db }) => {
return db.query("SELECT * FROM users ORDER BY id");
},
post: async (_, { id }, { db }) => {
const [post] = await db.query(
"SELECT * FROM posts WHERE id=$1 AND status='published'",
[id]
);
return post ?? null;
},
posts: async (_, { status, tagId, page = {} }, { db }) => {
const { limit = 10, offset = 0 } = page;
let sql = "SELECT p.* FROM posts p WHERE 1=1";
const params = [];
if (status) {
params.push(status.toLowerCase());
sql += ` AND p.status=$${params.length}`;
}
if (tagId) {
sql += ` AND EXISTS (
SELECT 1 FROM post_tags pt WHERE pt.post_id=p.id AND pt.tag_id=$${params.length + 1}
)`;
params.push(tagId);
}
params.push(limit, offset);
sql += ` ORDER BY p.published_at DESC LIMIT $${params.length - 1} OFFSET $${params.length}`;
return db.query(sql, params);
},
searchPosts: async (_, { query }, { db }) => {
return db.query(
`SELECT * FROM posts
WHERE status='published'
AND (title ILIKE $1 OR body ILIKE $1)
ORDER BY published_at DESC
LIMIT 20`,
[`%${query}%`]
);
},
tags: async (_, __, { db }) => db.query("SELECT * FROM tags ORDER BY name"),
},
// ── Field-level resolvers ────────────────────────────────────────────────
Post: {
// Use DataLoader → batched into 1 query for all posts
author: (post, _, { loaders }) => loaders.user.load(post.author_id),
tags: (post, _, { loaders }) => loaders.postTags.load(post.id),
likes: (post, _, { loaders }) => loaders.likesCount.load(post.id),
comments: async (post, _, { db }) => {
return db.query(
"SELECT * FROM comments WHERE post_id=$1 ORDER BY created_at",
[post.id]
);
},
isLikedBy: async (post, { userId }, { db }) => {
const [row] = await db.query(
"SELECT 1 FROM likes WHERE post_id=$1 AND user_id=$2",
[post.id, userId]
);
return !!row;
},
// Map snake_case DB column → camelCase GraphQL field
publishedAt: (post) => post.published_at,
},
Comment: {
author: (comment, _, { loaders }) => loaders.user.load(comment.author_id),
post: async (comment, _, { db }) => {
const [post] = await db.query("SELECT * FROM posts WHERE id=$1", [
comment.post_id,
]);
return post;
},
createdAt: (comment) => comment.created_at,
},
User: {
posts: async (user, _, { db }) => {
return db.query(
"SELECT * FROM posts WHERE author_id=$1 ORDER BY published_at DESC",
[user.id]
);
},
postCount: async (user, _, { db }) => {
const [{ count }] = await db.query(
"SELECT COUNT(*) AS count FROM posts WHERE author_id=$1 AND status='published'",
[user.id]
);
return Number(count);
},
},
// ── Mutations ─────────────────────────────────────────────────────────────
Mutation: {
createPost: async (_, { authorId, input }, { db }) => {
const { title, body, tagIds = [] } = input;
const [post] = await db.query(
`INSERT INTO posts (author_id, title, body, status)
VALUES ($1, $2, $3, 'draft')
RETURNING *`,
[authorId, title, body]
);
// Insert tag associations
if (tagIds.length > 0) {
const tagValues = tagIds.map((tid) => `(${post.id}, ${tid})`).join(",");
await db.query(
`INSERT INTO post_tags (post_id, tag_id) VALUES ${tagValues}`
);
}
return post;
},
publishPost: async (_, { id }, { db }) => {
const [post] = await db.query(
`UPDATE posts
SET status='published', published_at=NOW()
WHERE id=$1
RETURNING *`,
[id]
);
if (!post) throw new Error("Post not found");
return post;
},
likePost: async (_, { postId, userId }, { db, loaders }) => {
// INSERT OR IGNORE (idempotent)
await db.query(
`INSERT INTO likes (post_id, user_id) VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING`,
[postId, userId]
);
// Clear DataLoader cache so fresh count is fetched
loaders.likesCount.clear(postId);
const [post] = await db.query("SELECT * FROM posts WHERE id=$1", [
postId,
]);
pubsub.publish(`NEW_LIKE_${postId}`, { newLike: post });
return post;
},
addComment: async (_, { postId, authorId, body }, { db }) => {
const [comment] = await db.query(
`INSERT INTO comments (post_id, author_id, body)
VALUES ($1, $2, $3)
RETURNING *`,
[postId, authorId, body]
);
pubsub.publish(`NEW_COMMENT_${postId}`, { newComment: comment });
return comment;
},
},
// ── Subscriptions ─────────────────────────────────────────────────────────
Subscription: {
newComment: {
subscribe: (_, { postId }) =>
pubsub.asyncIterator([`NEW_COMMENT_${postId}`]),
},
newLike: {
subscribe: (_, { postId }) =>
pubsub.asyncIterator([`NEW_LIKE_${postId}`]),
},
},
};Problem 3.2 — GraphQL Query Writing Challenge
Write the GraphQL queries/mutations a client would send for these scenarios:
Scenario A: Load a blog post page
query PostPage($id: ID!, $currentUserId: ID!) {
post(id: $id) {
id
title
body
publishedAt
likes
isLikedBy(userId: $currentUserId)
author {
id
username
}
tags {
id
name
}
comments {
id
body
createdAt
author {
username
}
}
}
}Scenario B: Create and publish a post in two mutations
# Step 1: Create as draft
mutation CreateDraft($authorId: ID!) {
createPost(
authorId: $authorId
input: {
title: "My New Post"
body: "Content goes here..."
tagIds: ["1", "3"]
}
) {
id
title
status
}
}
# Step 2: Publish it
mutation Publish($id: ID!) {
publishPost(id: $id) {
id
status
publishedAt
}
}Scenario C: Real-time comment feed
subscription LiveComments($postId: ID!) {
newComment(postId: $postId) {
id
body
createdAt
author {
username
}
}
}Scenario D: Dashboard — multiple resources in one query
query Dashboard {
# Latest published posts
posts(status: PUBLISHED, page: { limit: 5, offset: 0 }) {
id
title
likes
author {
username
}
comments {
id
}
}
# All tags for filter sidebar
tags {
id
name
}
}Problem 3.3 — Spot the Bug (GraphQL Edition)
Prompt: Each snippet has a bug. Find and fix it.
Bug 1 — Missing DataLoader, causes N+1:
// ❌ Bug: resolver called once per post — N queries for N posts
Post: {
author: async (post, _, { db }) => {
const [user] = await db.query("SELECT * FROM users WHERE id=$1", [post.author_id]);
return user;
}
}
// ✅ Fix: use DataLoader
Post: {
author: (post, _, { loaders }) => loaders.user.load(post.author_id),
}Bug 2 — DataLoader returns in wrong order:
// ❌ Bug: DB query returns users in arbitrary order — DataLoader MUST return
// in the same order as the input IDs array!
new DataLoader(async (ids) => {
const users = await db.query("SELECT * FROM users WHERE id=ANY($1)", [ids]);
return users; // ← WRONG: DB order ≠ ids order
});
// ✅ Fix: map each id to its matching user
new DataLoader(async (ids) => {
const users = await db.query("SELECT * FROM users WHERE id=ANY($1)", [ids]);
return ids.map((id) => users.find((u) => u.id === Number(id)) ?? null);
// ↑ maps input ID order → correct user (or null if not found)
});Bug 3 — Mutation not returning updated data:
// ❌ Bug: DB update returns nothing — resolver returns undefined
likePost: async (_, { postId, userId }, { db }) => {
await db.query(
"INSERT INTO likes (post_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
[postId, userId]
);
// ← forgot to fetch and return the post!
};
// ✅ Fix: fetch post after mutation
likePost: async (_, { postId, userId }, { db }) => {
await db.query(
"INSERT INTO likes (post_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
[postId, userId]
);
const [post] = await db.query("SELECT * FROM posts WHERE id=$1", [postId]);
return post; // ← resolver must return the type declared in schema
};Key Concepts to Review
Self-Assessment Checklist
Use this before your interview:
Node.js
- [ ] Can write Express middleware from scratch
- [ ] Understand
next()vsnext(err)vsreturn - [ ] Can implement rate limiting with Map + timestamps
- [ ] Can implement debounce and throttle without lodash
- [ ] Know Promise.all, Promise.allSettled, Promise.race differences
- [ ] Can write JWT auth middleware with proper error handling
- [ ] Understand async queue / concurrency control
SQL
- [ ] Can design a normalized schema with proper FK constraints
- [ ] Know when to use
ON DELETE CASCADEvsRESTRICTvsSET NULL - [ ] Can write JOINs across 3+ tables
- [ ] Understand
GROUP BY+HAVINGvsWHERE - [ ] Can use window functions:
ROW_NUMBER,RANK,SUM OVER - [ ] Know when to add indexes and why (cardinality, query patterns)
- [ ] Understand
EXPLAIN/EXPLAIN ANALYZEbasics
GraphQL
- [ ] Can design a schema with types, enums, inputs, and root operations
- [ ] Know resolver signature:
(parent, args, context, info) - [ ] Can explain and solve the N+1 problem with DataLoader
- [ ] Know DataLoader's contract: batch fn must return in same order as IDs
- [ ] Understand subscriptions + PubSub pattern
- [ ] Can explain GraphQL error handling (always 200, errors array)
- [ ] Know when to add
context(auth, DB, loaders) vsargs
