DEV Community

Mohd Amir
Mohd Amir

Posted on

MERN + TypeScript Code Cheat Sheet — Part 2 (BullMQ, Puppeteer, SQL, Testing, React, Git/Docker/curl)

MERN Cheat Sheet — Part 2

(verify) = not in my repos and I'm not 100% sure of the API. Versions: see Part 1 (backend: bullmq ^6.3.11, ioredis ^6.0.0, vitest ^4.1.11, supertest ^7.1.4, mongodb-memory-server ^10.2.3; frontend: react ^19.2.8, vite ^8.3.0, react-router-dom ^7.18.4, @tanstack/react-query ^5.104.1, react-hook-form ^7.89.0, @hookform/resolvers ^5.9.1, axios ^1.20.0, tailwindcss ^4.3.3, vitest ^5.0.3). Puppeteer and MySQL are in neither repo.

Table of contents

  1. BullMQ + Redis
  2. Puppeteer PDF
  3. SQL (MySQL)
  4. Testing
  5. React + Vite + TypeScript
  6. Git, Docker and curl

6. BullMQ + Redis

6.1 Folder structure

src/
  config/redis.ts
  queues/pdf.queue.ts       # Queue instances (used by the API)
  jobs/pdf.job.ts           # job payload types + the processor function
  workers/pdf.worker.ts     # Worker wiring
  workers/index.ts          # separate process: starts all workers
Enter fullscreen mode Exit fullscreen mode

Gotcha: my repo currently keeps queue + worker together in src/queues/pdf/; the API process should only import queues, never workers.

6.2 ioredis connection

import { Redis } from "ioredis";
import { env } from "./env.js";

export const connection = new Redis({
  host: env.REDIS_HOST,
  port: env.REDIS_PORT,
  maxRetriesPerRequest: null,   // required for BullMQ Workers
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: my repo has maxRetriesPerRequest: 0 — BullMQ warns/errors for Workers; null = retry forever.

6.3 Queue

import { Queue } from "bullmq";
import { connection } from "../config/redis.js";

export interface PdfJobData { reportId: string; userId: string }

export const pdfQueue = new Queue<PdfJobData>("export-pdf", {
  connection,
  defaultJobOptions: { removeOnComplete: true },
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: the queue name is the contract — Queue and Worker must use the exact same string.

6.4 Add job (attempts, backoff, deterministic jobId)

await pdfQueue.add(
  "render",
  { reportId, userId },
  {
    jobId: `pdf-${reportId}`,                     // same id => no duplicate job
    attempts: 3,
    backoff: { type: "exponential", delay: 2000 },
    removeOnComplete: { age: 3600 },              // keep 1h so status stays queryable
    removeOnFail: { age: 24 * 3600 },
  },
);
Enter fullscreen mode Exit fullscreen mode

Gotcha: while a job with that jobId still exists, add is silently ignored — with removeOnComplete: true the id is freed immediately, so status lookups return nothing. Avoid : in custom ids (verify).

6.5 Worker with concurrency

import { Worker, type Job } from "bullmq";

export const pdfWorker = new Worker<PdfJobData>(
  "export-pdf",
  async (job: Job<PdfJobData>) => {
    await job.updateProgress(10);
    const path = await renderPdf(job.data.reportId);
    await job.updateProgress(100);
    return { path };                       // becomes job.returnvalue
  },
  { connection, concurrency: 2 },
);
Enter fullscreen mode Exit fullscreen mode

Gotcha: throw to fail/retry; the thrown error's message ends up in failedReason.

6.6 Job events

pdfWorker.on("completed", (job) => logger.info({ id: job.id }, "done"));
pdfWorker.on("failed", (job, err) => logger.error({ id: job?.id, err }, "failed"));
pdfWorker.on("error", (err) => logger.error({ err }, "worker error"));   // Redis/connection errors
Enter fullscreen mode Exit fullscreen mode

Gotcha: without an error listener, a Redis connection error can crash the process; in failed, job can be undefined.

6.7 Job status endpoint

router.get("/exports/:reportId", authenticate, asyncHandler(async (req, res) => {
  const job = await pdfQueue.getJob(`pdf-${req.params.reportId}`);
  if (!job) throw new ApiError(404, "Job not found", "NOT_FOUND");

  res.json({ success: true, data: {
    state: await job.getState(),          // waiting | active | completed | failed | delayed
    progress: job.progress,
    result: job.returnvalue,
    error: job.failedReason,
  } });
}));
Enter fullscreen mode Exit fullscreen mode

Gotcha: check job.data.userId === req.auth.userId before returning — otherwise anyone can read anyone's job.

6.8 Separate worker process (src/workers/index.ts)

import { connectDatabase, disconnectDatabase } from "../config/db.js";
import { connection } from "../config/redis.js";
import { pdfWorker } from "./pdf.worker.js";

const workers = [pdfWorker];

await connectDatabase();                    // top-level await OK with ESM

async function shutdown() {
  await Promise.all(workers.map((w) => w.close()));   // waits for active jobs to finish
  await disconnectDatabase();
  await connection.quit();
}
for (const sig of ["SIGINT", "SIGTERM"] as const) {
  process.once(sig, () => { shutdown().then(() => process.exit(0)).catch(() => process.exit(1)); });
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: add "worker": "tsx watch src/workers/index.ts" / "start:worker": "node dist/workers/index.js" scripts; scale workers by running more processes.

6.9 Graceful close (API side)

await pdfQueue.close();
await connection.quit();
Enter fullscreen mode Exit fullscreen mode

Gotcha: worker.close() waits for in-flight jobs; worker.close(true) force-closes and the job is retried as stalled.

6.10 docker-compose for Redis + Mongo

services:
  mongo:
    image: mongo:7
    ports: ["27017:27017"]
    volumes: [mongo_data:/data/db]
  redis:
    image: redis:7
    ports: ["6479:6379"]        # host 6479 -> container 6379 (matches REDIS_PORT default)
    volumes: [redis_data:/data]
volumes:
  mongo_data:
  redis_data:
Enter fullscreen mode Exit fullscreen mode

Gotcha: inside another container use the service name (redis, mongo), not localhost.


7. Puppeteer PDF

(Not in my repos — npm i puppeteer, latest stable assumed, (verify) on option details.)

7.1 Launch + render

import puppeteer from "puppeteer";

const browser = await puppeteer.launch({
  headless: true,
  args: ["--no-sandbox", "--disable-setuid-sandbox"],   // needed in most Docker setups
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: --no-sandbox is for containers; don't use it where you can run the sandbox.

7.2 setContent + pdf options

const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });

const pdf = await page.pdf({
  format: "A4",
  printBackground: true,                    // otherwise CSS backgrounds are dropped
  margin: { top: "20mm", right: "15mm", bottom: "20mm", left: "15mm" },
  displayHeaderFooter: false,
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: page.pdf returns a Uint8Array in recent versions (older: Buffer) — wrap with Buffer.from(pdf) before res.send/fs.writeFile (verify).

7.3 Close page and browser

const page = await browser.newPage();
try {
  await page.setContent(html);
  return await page.pdf({ format: "A4" });
} finally {
  await page.close();       // always, even on error
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: leaked pages/browsers are leaked Chromium processes (hundreds of MB each).

7.4 Reuse one browser

import type { Browser } from "puppeteer";

let browserPromise: Promise<Browser> | undefined;
export const getBrowser = () => (browserPromise ??= puppeteer.launch({ args: ["--no-sandbox"] }));

export async function closeBrowser() {
  if (browserPromise) await (await browserPromise).close();
  browserPromise = undefined;
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: store the promise, not the browser, so concurrent first calls don't launch twice; call closeBrowser() on shutdown.

7.5 Inside a BullMQ worker

export const pdfWorker = new Worker<PdfJobData>("export-pdf", async (job) => {
  const browser = await getBrowser();
  const page = await browser.newPage();
  try {
    await page.setContent(await buildHtml(job.data.reportId), { waitUntil: "networkidle0" });
    const pdf = await page.pdf({ format: "A4", printBackground: true });
    await fs.writeFile(`exports/${job.data.reportId}.pdf`, pdf);
    return { path: `exports/${job.data.reportId}.pdf` };
  } finally {
    await page.close();
  }
}, { connection, concurrency: 2 });
Enter fullscreen mode Exit fullscreen mode

Gotcha: concurrency = concurrent pages in one browser; add await closeBrowser() to the worker's shutdown (6.8).


8. SQL (MySQL)

No SQL in my repos; MySQL 8 assumed. Illustrative tables: employees(id, name, dept_id, salary, manager_id), departments(id, name), orders(id, customer_id, total, created_at).

8.1 INNER JOIN / LEFT JOIN

SELECT e.name, d.name AS dept
FROM employees e
INNER JOIN departments d ON d.id = e.dept_id;     -- only matching rows

SELECT d.name, e.name
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id;        -- all departments, NULL if no employees
Enter fullscreen mode Exit fullscreen mode

Gotcha: a WHERE on the right table's column turns a LEFT JOIN into an INNER JOIN — put the condition in ON.

8.2 GROUP BY + HAVING

SELECT dept_id, COUNT(*) AS n, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id
HAVING COUNT(*) > 5
ORDER BY avg_salary DESC;
Enter fullscreen mode Exit fullscreen mode

Gotcha: WHERE filters rows before grouping; HAVING filters groups after.

8.3 Subqueries

SELECT name FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);                 -- scalar

SELECT name FROM employees
WHERE dept_id IN (SELECT id FROM departments WHERE name = 'Sales'); -- IN

SELECT d.name FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.id);    -- correlated
Enter fullscreen mode Exit fullscreen mode

Gotcha: prefer EXISTS over IN for large subqueries; NOT IN returns nothing if the subquery has a NULL.

8.4 Window functions: ROW_NUMBER, RANK

SELECT name, dept_id, salary,
  ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
  RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk
FROM employees;
Enter fullscreen mode Exit fullscreen mode

Gotcha: ROW_NUMBER never ties; RANK ties then skips (1,1,3); DENSE_RANK ties without skipping (1,1,2).

8.5 Top-N per group

SELECT * FROM (
  SELECT e.*, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM employees e
) t
WHERE rn <= 3;
Enter fullscreen mode Exit fullscreen mode

Gotcha: you can't filter on a window function in the same WHERE — wrap it in a subquery/CTE.

8.6 Second-highest value

SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1;

-- NULL if there is no second value:
SELECT (SELECT DISTINCT salary FROM employees ORDER BY salary DESC LIMIT 1 OFFSET 1) AS second_highest;

-- with ties handled:
SELECT salary FROM (SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS r FROM employees) t WHERE r = 2 LIMIT 1;
Enter fullscreen mode Exit fullscreen mode

Gotcha: without DISTINCT, duplicates of the top salary count as "second".

8.7 Composite and covering indexes

CREATE INDEX idx_orders_cust_date ON orders (customer_id, created_at);           -- composite
CREATE INDEX idx_orders_cover     ON orders (customer_id, created_at, total);    -- covering
Enter fullscreen mode Exit fullscreen mode

Gotcha: leftmost-prefix rule — (customer_id, created_at) helps customer_id alone but not created_at alone; "covering" = every selected column is in the index, so no table lookup (Using index in EXPLAIN).

8.8 EXPLAIN

EXPLAIN SELECT total FROM orders WHERE customer_id = 42 ORDER BY created_at DESC;
-- EXPLAIN ANALYZE <query>;   -- MySQL 8.0.18+: actually runs it, shows real timings
Enter fullscreen mode Exit fullscreen mode

Gotcha: look at type (ALL = full scan; ref/range = good), key (index used), rows, and Extra (Using filesort / Using temporary = bad signs).

8.9 Transactions

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;      -- or ROLLBACK;
Enter fullscreen mode Exit fullscreen mode

Gotcha: needs InnoDB; DDL statements (CREATE/ALTER) implicitly commit. Use SELECT ... FOR UPDATE to lock rows you're about to change.


9. Testing

9.1 Vitest config + env setup

// vitest.config.ts  (verify: backend config wasn't in my dump; I only saw tests/setup-env.ts)
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: { environment: "node", setupFiles: ["./tests/setup-env.ts"], hookTimeout: 60_000 },
});
Enter fullscreen mode Exit fullscreen mode
// tests/setup-env.ts
process.env.NODE_ENV = "test";
process.env.MONGO_URI = "mongodb://127.0.0.1:27017/auth_test";
process.env.LOG_LEVEL = "silent";
process.env.ACCESS_TOKEN_SECRET = "test-access-secret-with-at-least-32-characters";
process.env.REFRESH_TOKEN_SECRET = "test-refresh-secret-with-at-least-32-characters";
process.env.AUTH_COOKIE_SECURE = "false";
Enter fullscreen mode Exit fullscreen mode

Gotcha: env must be set before env.ts is imported — that's why it's a setupFiles entry; first run downloads the MongoDB binary, hence the long hookTimeout.

9.2 mongodb-memory-server setup + teardown

import { afterAll, afterEach, beforeAll } from "vitest";
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";

let mongo: MongoMemoryServer;

beforeAll(async () => {
  mongo = await MongoMemoryServer.create();
  await mongoose.connect(mongo.getUri());
  await Promise.all([UserModel.init(), SessionModel.init(), ItemModel.init()]);  // build unique indexes
});

afterEach(async () => {
  await Promise.all([UserModel.deleteMany({}), SessionModel.deleteMany({}), ItemModel.deleteMany({})]);
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongo.stop();
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: call Model.init() or unique-index tests (409) can flake because indexes build lazily.

9.3 supertest + authenticated request helper

import request from "supertest";
import { app } from "../src/app.js";

async function register(email: string) {
  const res = await request(app).post("/api/auth/register")
    .send({ email, password: "correct horse battery" });
  expect(res.status).toBe(201);
  return { userId: res.body.data.user._id as string, accessToken: res.body.data.accessToken as string };
}

const authed = (method: "get" | "post" | "patch" | "delete", path: string, token: string) =>
  request(app)[method](path).set("Authorization", `Bearer ${token}`);

// const { accessToken } = await register("a@example.com");
// const res = await authed("post", "/api/items", accessToken).send({ title: "x" });
Enter fullscreen mode Exit fullscreen mode

Gotcha: use request(app) (not a running server); for cookies, copy set-cookie[0].split(";")[0] into .set("Cookie", ...).

9.4 Cross-user access test

it("returns 404 for cross-owner read, update, delete", async () => {
  const owner = await register("owner@example.com");
  const stranger = await register("stranger@example.com");
  const created = await authed("post", "/api/items", owner.accessToken).send({ title: "private" });
  const id = created.body.data.item._id;

  expect((await authed("get", `/api/items/${id}`, stranger.accessToken)).status).toBe(404);
  expect((await authed("patch", `/api/items/${id}`, stranger.accessToken).send({ title: "stolen" })).status).toBe(404);
  expect((await authed("delete", `/api/items/${id}`, stranger.accessToken)).status).toBe(404);

  const unchanged = await ItemModel.findById(id);
  expect(unchanged?.title).toBe("private");
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: always re-read the DB at the end — a 404 alone doesn't prove nothing changed.

9.5 Validation test

it("rejects bad fields and owner injection", async () => {
  const { accessToken } = await register("v@example.com");
  const bodies = [
    { title: "bad rating", rating: 6 },
    { title: "   " },
    { title: "unknown", arbitrary: true },
    { title: "inject", owner: "507f1f77bcf86cd799439011" },
  ];
  for (const body of bodies) {
    expect((await authed("post", "/api/items", accessToken).send(body)).status).toBe(400);
  }
  expect(await ItemModel.countDocuments({})).toBe(0);
});

it("rejects NoSQL operator objects", async () => {
  const res = await request(app).post("/api/auth/login")
    .send({ email: { $gt: "" }, password: "correct horse battery" });
  expect(res.status).toBe(400);
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: z.strictObject is what blocks unknown/owner keys; Zod type checks (z.string()) block { $gt: "" }.


10. React + Vite + TypeScript

10.1 Setup with Tailwind v4

npm create vite@latest app -- --template react-ts
npm i tailwindcss @tailwindcss/vite axios @tanstack/react-query react-router-dom react-hook-form @hookform/resolvers zod
Enter fullscreen mode Exit fullscreen mode
// vite.config.ts
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  server: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true } } },
  test: { environment: "jsdom", setupFiles: ["./src/test/setup.ts"], restoreMocks: true },
});
Enter fullscreen mode Exit fullscreen mode
/* src/index.css */
@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

Gotcha: no tailwind.config.js/PostCSS needed in v4; the /api proxy makes the browser see same-origin (no CORS, cookies just work).

10.2 Router with protected and public-only routes

<Routes>
  <Route element={<AppLayout />}>
    <Route index element={<Navigate to="/dashboard" replace />} />
    <Route element={<PublicOnlyRoute />}>
      <Route path="/login" element={<LoginPage />} />
      <Route path="/register" element={<RegisterPage />} />
    </Route>
    <Route element={<ProtectedRoute />}>
      <Route path="/dashboard" element={<DashboardPage />} />
    </Route>
    <Route path="*" element={<NotFoundPage />} />
  </Route>
</Routes>
Enter fullscreen mode Exit fullscreen mode
export function ProtectedRoute() {
  const { isAuthenticated, isBooting } = useAuth();
  const location = useLocation();
  if (isBooting) return <Spinner />;                                   // wait for session restore
  if (!isAuthenticated) return <Navigate to="/login" replace state={{ from: location }} />;
  return <Outlet />;
}
// PublicOnlyRoute: same, but isAuthenticated ? <Navigate to="/dashboard" replace /> : <Outlet />
Enter fullscreen mode Exit fullscreen mode

Gotcha: without the isBooting gate you'd redirect to /login on every page refresh before the cookie session is restored. My repo imports from react-router-dom; v7 also exports the same from react-router (verify).

10.3 axios client + interceptors

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || "/api",
  withCredentials: true,
});

apiClient.interceptors.request.use((config) => {
  const token = getAccessToken();                 // in-memory variable
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

apiClient.interceptors.response.use((r) => r, async (error: AxiosError<ErrorResponse>) => {
  const config = error.config as (InternalAxiosRequestConfig & { _authRetry?: boolean }) | undefined;
  const isAuthRoute = /(?:^|\/)auth(?:\/|$)/.test(`${config?.baseURL ?? ""}${config?.url ?? ""}`);

  if (error.response?.status !== 401 || !config || config._authRetry || isAuthRoute) {
    return Promise.reject(error);
  }
  config._authRetry = true;                       // retry once only
  try {
    await refreshAccessToken();
    return await apiClient.request(config);
  } catch (e) {
    setAccessToken(null);
    window.dispatchEvent(new Event("auth:expired"));
    return Promise.reject(e);
  }
});
Enter fullscreen mode Exit fullscreen mode

Gotcha: skip /auth/* routes in the retry logic or a failed login/refresh loops forever; keep the access token in memory, not localStorage.

10.4 Single-flight refresh + navigator.locks

let inFlight: Promise<string> | null = null;

export function refreshAccessToken(): Promise<string> {
  if (inFlight) return inFlight;                                  // same tab: share one request

  const request = async () => {
    const res = await axios.post<SuccessResponse<RefreshData>>(
      `${apiBaseUrl}/auth/refresh`, undefined, { withCredentials: true });
    setAccessToken(res.data.data.accessToken);
    return res.data.data.accessToken;
  };

  const pending = navigator.locks
    ? navigator.locks.request("auth-refresh", request)            // across tabs: one at a time
    : request();

  inFlight = pending;
  const clear = () => { if (inFlight === pending) inFlight = null; };
  void pending.then(clear, clear);
  return pending;
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: use the bare axios (not apiClient) so the 401 interceptor can't recurse; this exists because the backend treats a reused refresh token as theft and kills the session.

10.5 TanStack Query: client + key factory

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 60_000, retry: (n, e) => !(axios.isAxiosError(e) && (e.response?.status ?? 500) < 500) && n < 2 },
    mutations: { retry: false },
  },
});

export const itemKeys = {
  all: ["items"] as const,
  lists: () => [...itemKeys.all, "list"] as const,
  list: (q: ItemQuery) => [...itemKeys.lists(), q] as const,
  details: () => [...itemKeys.all, "detail"] as const,
  detail: (id: string) => [...itemKeys.details(), id] as const,
  stats: () => [...itemKeys.all, "stats"] as const,
};
Enter fullscreen mode Exit fullscreen mode

Gotcha: hierarchical keys let invalidateQueries({ queryKey: itemKeys.lists() }) hit every list at once; wrap the app in <QueryClientProvider client={queryClient}>.

10.6 useQuery + keepPreviousData

export function useItems(query: ItemQuery) {
  return useQuery({
    queryKey: itemKeys.list(query),
    queryFn: async ({ signal }) => {
      const res = await apiClient.get<SuccessResponse<ItemListData>>("/items", { params: query, signal });
      return res.data.data;
    },
    placeholderData: keepPreviousData,         // keep old page visible while the next loads
  });
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: pass signal so cancelled queries abort the HTTP request; in v5 keepPreviousData is a placeholderData value, not an option.

10.7 useMutation + invalidate

export function useCreateItem() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async (input: CreateItemInput) =>
      (await apiClient.post<SuccessResponse<{ item: Item }>>("/items", input)).data.data.item,
    onSuccess: () => Promise.all([
      qc.invalidateQueries({ queryKey: itemKeys.lists() }),
      qc.invalidateQueries({ queryKey: itemKeys.stats() }),
    ]),
  });
}
// await createItem.mutateAsync(input)  (throws)   |   createItem.mutate(input)  (fire and forget)
Enter fullscreen mode Exit fullscreen mode

Gotcha: return the promise from onSuccess so isPending stays true until the refetch finishes.

10.8 Optimistic update with rollback

export function useDeleteItem() {
  const qc = useQueryClient();
  return useMutation<string, Error, string, { prev: Array<[QueryKey, ItemListData | undefined]> }>({
    mutationFn: async (id) => { await apiClient.delete(`/items/${id}`); return id; },
    onMutate: async (id) => {
      await qc.cancelQueries({ queryKey: itemKeys.lists() });             // stop in-flight refetches
      const prev = qc.getQueriesData<ItemListData>({ queryKey: itemKeys.lists() });
      for (const [key, data] of prev) {
        if (!data) continue;
        qc.setQueryData<ItemListData>(key, {
          ...data, items: data.items.filter((i) => i._id !== id), total: Math.max(0, data.total - 1),
        });
      }
      return { prev };
    },
    onError: (_e, _id, ctx) => { for (const [key, data] of ctx?.prev ?? []) qc.setQueryData(key, data); },
    onSettled: () => qc.invalidateQueries({ queryKey: itemKeys.lists() }),
  });
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: snapshot in onMutate, restore in onError, always re-sync in onSettled; cancelQueries first or a refetch overwrites the optimistic data.

10.9 react-hook-form + zod resolver

const schema = z.object({
  email: z.email("Enter a valid email"),
  password: z.string().min(12, "At least 12 characters"),
});
type Values = z.infer<typeof schema>;

const { register, handleSubmit, setError, formState: { errors, isSubmitting } } =
  useForm<Values>({ resolver: zodResolver(schema) });

const onSubmit = async (values: Values) => {
  try { await login(values); }
  catch (e) { setError("email", { type: "server", message: "Email or password is incorrect" }); }
};

<form noValidate onSubmit={handleSubmit(onSubmit)}>
  <input type="email" {...register("email")} aria-invalid={Boolean(errors.email)} />
  {errors.email && <p role="alert">{errors.email.message}</p>}
</form>
Enter fullscreen mode Exit fullscreen mode

Gotcha: if a field is transformed (e.g. z.string().transform(Number)) use z.input<typeof schema> for useForm's type; map server details[].path back with setError. z.email() is the Zod 4 form (my repo still uses z.string().email()).

10.10 AuthContext with useAuth

export const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isBooting, setIsBooting] = useState(true);

  useEffect(() => {                                   // restore session from the refresh cookie
    let active = true;
    (async () => {
      try {
        await refreshAccessToken();
        const res = await apiClient.get<SuccessResponse<{ user: User }>>("/users/me");
        if (active) setUser(res.data.data.user);
      } catch { setAccessToken(null); }
      finally { if (active) setIsBooting(false); }
    })();
    return () => { active = false; };
  }, []);

  const value = useMemo(() => ({ user, isAuthenticated: user !== null, isBooting, login, logout }), [user, isBooting]);
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
  return ctx;
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: AuthProvider must sit inside <BrowserRouter> if it calls useNavigate. My useMemo omits login/logout from deps; that's only safe because they don't close over changing state — wrap them in useCallback to be lint-clean.

10.11 useEffect cleanup

useEffect(() => {
  const onExpired = () => navigate("/login", { replace: true });
  window.addEventListener("auth:expired", onExpired);
  return () => window.removeEventListener("auth:expired", onExpired);
}, [navigate]);
Enter fullscreen mode Exit fullscreen mode

Rule: anything you subscribe, start or fetch in an effect gets undone in its return (listeners, timers, active = false flags, AbortController).

10.12 useMemo

const visible = useMemo(() => items.filter((i) => i.status === status), [items, status]);
Enter fullscreen mode Exit fullscreen mode

Rule: only for expensive computations or to keep an object/array identity stable for a child/context; not for cheap math.

10.13 useCallback

const handleDelete = useCallback((id: string) => deleteItem.mutate(id), [deleteItem.mutate]);
Enter fullscreen mode Exit fullscreen mode

Rule: only useful when the function is passed to a memoized child (React.memo) or used as an effect dependency.

10.14 useRef

const inputRef = useRef<HTMLInputElement>(null);
const timer = useRef<number | undefined>(undefined);

inputRef.current?.focus();
timer.current = window.setTimeout(save, 500);
Enter fullscreen mode Exit fullscreen mode

Rule: a ref holds a mutable value or DOM node that persists across renders and changing it does NOT re-render; use state if the UI must update.


11. Git, Docker and curl

11.1 Init + commit conventions

git init -b main
git add -A
git commit -m "feat(auth): add refresh token rotation"
# types: feat | fix | docs | refactor | test | chore | perf
git switch -c feat/sessions-page
git push -u origin feat/sessions-page
Enter fullscreen mode Exit fullscreen mode

Gotcha: Conventional Commits = type(scope): imperative summary; one logical change per commit.

11.2 .gitignore for Node

node_modules/
dist/
build/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
.vscode/
Enter fullscreen mode Exit fullscreen mode

Gotcha: if .env was already committed, git rm --cached .env and rotate the secrets.

11.3 Docker Compose basics

docker compose up -d            # start in background
docker compose ps
docker compose logs -f redis
docker compose exec mongo mongosh
docker compose down             # stop + remove containers (keeps volumes)
docker compose down -v          # ...and delete volumes (wipes data)
Enter fullscreen mode Exit fullscreen mode

Gotcha: up reuses existing containers; add --build after changing a Dockerfile.

11.4 Dockerfile for the API (verify)

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
Enter fullscreen mode Exit fullscreen mode

Gotcha: copy package*.json first so the npm ci layer is cached; add .dockerignore with node_modules.

11.5 curl: JSON

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"a@example.com","password":"correct horse battery"}'
Enter fullscreen mode Exit fullscreen mode

Gotcha: add -i to see headers/Set-Cookie, -s | jq for pretty output.

11.6 curl: cookies (-c and -b)

curl -c cookies.txt -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" -d '{"email":"a@example.com","password":"correct horse battery"}'

curl -b cookies.txt -c cookies.txt -X POST http://localhost:3000/api/auth/refresh
Enter fullscreen mode Exit fullscreen mode

Gotcha: -c writes the cookie jar, -b sends it; with secure: true cookies, curl over plain http://localhost may not send them back (use AUTH_COOKIE_SECURE=false in dev).

11.7 curl: bearer auth

TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"a@example.com","password":"correct horse battery"}' | jq -r '.data.accessToken')

curl http://localhost:3000/api/items?status=open -H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:3000/api/items -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"title":"first item","rating":4}'
Enter fullscreen mode Exit fullscreen mode

Gotcha: quote URLs that contain ? or & in zsh/bash scripts.

Top comments (0)