Skip to content

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:

js
// 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:

js
// 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:

js
// 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:

js
// ── 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:

sql
-- 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_price in order_items is a snapshot — if product price changes later, historical orders remain accurate.
  • parent_id in categories enables nested categories (Electronics → Phones → Android).
  • ON DELETE RESTRICT on order_items.product_id prevents 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)

sql
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

sql
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

sql
-- 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)

sql
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)

sql
-- 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:

sql
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):

sql
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:

graphql
# ── 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:

js
// 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

graphql
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

graphql
# 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

graphql
subscription LiveComments($postId: ID!) {
  newComment(postId: $postId) {
    id
    body
    createdAt
    author {
      username
    }
  }
}

Scenario D: Dashboard — multiple resources in one query

graphql
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:

js
// ❌ 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:

js
// ❌ 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:

js
// ❌ 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() vs next(err) vs return
  • [ ] 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 CASCADE vs RESTRICT vs SET NULL
  • [ ] Can write JOINs across 3+ tables
  • [ ] Understand GROUP BY + HAVING vs WHERE
  • [ ] Can use window functions: ROW_NUMBER, RANK, SUM OVER
  • [ ] Know when to add indexes and why (cardinality, query patterns)
  • [ ] Understand EXPLAIN / EXPLAIN ANALYZE basics

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) vs args

Released under the ISC License.