Practice Questions

1. What is the difference between dependencies and devDependencies in package.json? Give two examples of packages that belong in each, and show the install command for each type.

Solution
  • dependencies are required at runtime. Your application needs these to function (e.g., express, lodash).
  • devDependencies are only needed during development: things like test runners, linters, and build tools. They do not ship with production code (e.g., jest, eslint).
# Production dependency
npm install express

# Development dependency
npm install --save-dev jest
# or equivalently: npm install -D jest

2. Explain semantic versioning. What does each part of the version 2.4.1 represent? Then explain the difference between ^2.4.1 and ~2.4.1 in package.json.

Solution

Semantic versioning uses the format MAJOR.MINOR.PATCH:

  • MAJOR (2): incremented for breaking changes (incompatible API changes)
  • MINOR (4): incremented for new features that are backwards compatible
  • PATCH (1): incremented for backwards-compatible bug fixes

The version range symbols control what updates are allowed:

  • ^2.4.1 (caret) – allows minor and patch updates: >=2.4.1 and <3.0.0. This is the default when you run npm install.
  • ~2.4.1 (tilde): more conservative, only allows patch updates: >=2.4.1 and <2.5.0.

3. What is the purpose of package-lock.json? Why should you always commit it to version control?

Solution

package-lock.json records the exact version of every installed package (including transitive dependencies). Without it, version ranges like ^2.4.1 can resolve to different actual versions depending on when you run npm install. Your machine might get 2.4.1 today and a teammate might get 2.4.7 next week.

You should always commit it because it ensures everyone on the team – and your CI/CD pipeline – gets identical dependencies. This makes builds reproducible and prevents “works on my machine” issues caused by version drift.

4. Name the three Vite commands a typical project uses (dev, build, preview). What does each one do? Then explain what Hot Module Replacement (HMR) is and why it matters during development.

Solution
npm run dev      # Start a development server with hot reload
npm run build    # Build optimized output for production
npm run preview  # Preview the production build locally

Hot Module Replacement (HMR) is a development feature where changes you make to your source code are reflected in the browser in milliseconds, without a full page reload. The dev server detects which module changed and swaps just that module, preserving application state. This makes the development feedback loop much faster compared to traditional full-page reloads.

5. What is tree shaking? Why does it work better with ES module syntax (import/export) than with CommonJS (require)?

Solution

Tree shaking is the process of eliminating code that is imported but never actually used. The name comes from the idea of shaking a tree so the dead leaves fall off.

// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }

// app.js -- only `add` is used, so `multiply` is removed from the bundle
import { add } from "./math.js";

It works better with ES modules because import/export statements are static – the bundler can analyze at build time exactly which exports are used and which are not. CommonJS require() calls are dynamic (they can appear inside conditionals or use computed paths), so the bundler cannot always determine which exports are actually needed.

6. Write a Jest test file for the following module. Use describe to group tests, write at least three test cases (including an edge case), and follow the Arrange-Act-Assert pattern.

// calculator.js
function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}
module.exports = { divide };
Solution
// calculator.test.js
const { divide } = require("./calculator");

describe("divide", () => {
  test("divides two positive numbers", () => {
    // Arrange
    const a = 10, b = 2;
    // Act
    const result = divide(a, b);
    // Assert
    expect(result).toBe(5);
  });

  test("handles negative numbers", () => {
    expect(divide(-10, 2)).toBe(-5);
  });

  test("returns a decimal result", () => {
    expect(divide(7, 2)).toBe(3.5);
  });

  test("throws when dividing by zero", () => {
    expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
  });
});

Key points: toBe is used for primitive values (numbers), toThrow is used to assert that a function throws, and you must wrap the throwing call in () => so Jest can catch the error.

7. What is the difference between toBe and toEqual in Jest? When would you use each? Show an example where using the wrong one would cause a test to fail.

Solution
  • toBe uses strict equality (===). It works for primitives (numbers, strings, booleans) and checks reference identity for objects.
  • toEqual performs a deep comparison. It checks that two objects or arrays have the same structure and values, regardless of whether they are the same reference.
// This PASSES -- primitives work with toBe
expect(1 + 2).toBe(3);

// This FAILS -- different object references
expect({ name: "Ali" }).toBe({ name: "Ali" }); // FAIL!

// This PASSES -- toEqual checks structure, not reference
expect({ name: "Ali" }).toEqual({ name: "Ali" }); // PASS

Rule of thumb: use toBe for primitives, use toEqual for objects and arrays.

8. Create a mock function using jest.fn() that simulates an API call. Make it return a resolved promise with { id: 1, name: "Alice" }. Then write a test that calls the mock and verifies both the return value and that the function was called exactly once.

Solution
test("fetches user data from mock API", async () => {
  // Arrange
  const fetchUser = jest.fn();
  fetchUser.mockResolvedValue({ id: 1, name: "Alice" });

  // Act
  const user = await fetchUser(1);

  // Assert
  expect(user).toEqual({ id: 1, name: "Alice" });
  expect(fetchUser).toHaveBeenCalledTimes(1);
  expect(fetchUser).toHaveBeenCalledWith(1);
});

mockResolvedValue makes the mock return a resolved promise, which is the async equivalent of mockReturnValue. You can also use mockRejectedValue to simulate failed API calls.

9. What is the difference between linting and formatting? Which tool handles each concern in a typical project? Explain why you need both.

Solution
Concern Tool What It Does
Code quality and bugs ESLint (linter) Catches unused variables, enforces ===, flags potential bugs
Code style and appearance Prettier (formatter) Fixes indentation, quote style, line breaks, trailing commas

ESLint checks whether the code is correct. Prettier checks whether the code is formatted consistently.

You need both because Prettier will format code that has bugs without complaining (like using == instead of === or declaring unused variables), and ESLint does not care whether your indentation is consistent. ESLint catches logic and quality issues, and Prettier handles visual consistency.

10. Write an ESLint flat config (eslint.config.js) that extends the recommended rules, then sets eqeqeq to "error", no-unused-vars to "warn", and no-console to "off". Also ignore the dist/ directory.

Solution
// eslint.config.js
import js from "@eslint/js";

export default [
  {
    ignores: ["dist/**"],
  },
  js.configs.recommended,
  {
    rules: {
      eqeqeq: "error",
      "no-unused-vars": "warn",
      "no-console": "off",
    },
  },
];

The flat config (ESLint 9+) is an array of configuration objects. js.configs.recommended provides a sensible set of default rules. Custom rules are layered on top, and ignores prevents linting of build output.

11. Describe how to set up a pre-commit hook that automatically lints and formats staged files before each commit. What tools are involved, and what goes in the configuration?

Solution

You need two tools: Husky (for Git hooks) and lint-staged (for running commands on staged files only).

Setup:

npm install --save-dev husky lint-staged
npx husky init

Add lint-staged configuration to package.json:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,css}": ["prettier --write"]
  }
}

Edit the Husky pre-commit hook (.husky/pre-commit):

npx lint-staged

Now every commit automatically lints and formats only the files being committed. If ESLint finds unfixable errors, the commit is rejected, which keeps the repository clean.

12. You need to choose a bundler for each of the following scenarios. For each one, name the best tool and briefly explain why.

(a) A brand-new single-page web application (b) A reusable utility library you want to publish on npm © A quick prototype where you want zero configuration (d) An existing enterprise app with complex custom build requirements

Solution

(a) New web app – Vite. It has a fast dev server with HMR, supports TypeScript and JSX out of the box, and produces optimized production builds with minimal configuration.

(b) npm library – Rollup. It is designed for building libraries. It is built around ES modules, so it produces small output with little extra wrapper code, and it supports multiple output formats (ESM, CJS, UMD).

© Quick prototype – Parcel. It requires zero configuration. Point it at your HTML file, and it detects the needed setup, including TypeScript, CSS, and HMR.

(d) Complex enterprise app – Webpack. It has the largest plugin ecosystem and gives you fine-grained control over the build. It is the best choice when you need that much configurability, or when you are maintaining an existing Webpack codebase.