Skip to content

Troubleshooting

rocambille edited this page Aug 1, 2026 · 6 revisions

Summary: This page helps you answer the most frequently asked questions both about StartER's architecture and about common errors.

What you'll learn:

  • Understand the design rationale behind key StartER architectural patterns
  • Diagnose and resolve common runtime, routing, and schema errors
  • Troubleshoot SSR, CSRF, and authentication issues quickly

Common architectural misconceptions

Before debugging operational errors, understanding why StartER is designed this way helps prevent common architectural pitfalls.

Misconception 1: "Why shouldn't I write SQL queries and res.json() directly inside my route handlers?"

In basic Express tutorials, it is common to write database queries and res.json() directly inside route callbacks. StartER intentionally separates concerns into three distinct layers:

  • Repositories (itemRepository.ts) handle raw SQL queries and runtime Zod schema parsing (z.ZodType<Item>), remaining completely isolated from HTTP requests or routes.
  • Input sanitization lives in Validators (itemValidator.ts), validating untrusted client input before it reaches controller code.
  • Actions (itemActions.ts) stay lean (~5 lines), focusing strictly on HTTP status codes and response headers.

Misconception 2: "Why doesn't make:clone automatically register routes or merge cloned modules into generic helpers?"

StartER adheres strictly to a Zero-Magic design philosophy:

  1. Explicit Route Registration: Automatic AST/regex route patching scripts are fragile and hide application control flow. Requiring explicit imports in src/express/routes.ts and src/react/routes.tsx ensures you always know how routes are wired.
  2. Code vs. Intention: make:clone duplicates code, not intention. Each cloned domain module (item, task, user) has its own distinct business lifecycle and evolving domain rules. Forcing premature DRY abstraction onto separate domain modules creates tight coupling: modifying a shared generic helper for one module risks breaking other modules.

Misconception 3: "Why isn't user_id included in the Zod client validation schema?"

Allowing clients to submit user_id in request payloads invites privilege escalation attacks (e.g. malicious callers forging { "user_id": 999 }).

StartER uses a 3-step validation pipeline:

  1. schema.safeParse(req.body) validates only untrusted fields submitted by the client (e.g. { title: "My item" }).
  2. inject(req) injects trusted server-side context (e.g. { user_id: req.me.id }).
  3. req.body is replaced with the sanitized, merged result.

The client Zod schema defines only what the client is permitted to send.

Misconception 4: "Why use synchronous node:sqlite instead of async promises or an ORM like Prisma/TypeORM?"

StartER uses Node.js 22+ native node:sqlite in synchronous mode for pedagogical clarity and performance:

  • Eliminates 90% of async/await overhead, unhandled promise rejections, and ORM query magic.
  • Data access methods read like pure, synchronous TypeScript functions.
  • Prevents BigInt serialization crashes by coercing SQLite IDs to standard numbers (type RowId = number).

Misconception 5: "Why does CSRF protection return 401 Unauthorized instead of 403 Forbidden?"

When a CSRF header is missing or mismatched, StartER's csrf middleware returns 401 Unauthorized rather than 403 Forbidden.

This is a deliberate security choice: returning 401 makes CSRF failures indistinguishable from session JWT failures, preventing attackers from probing whether a session cookie is valid.

Frequent errors & symptoms

Route not found (404 on a new endpoint)

Symptom: you created a new module with make:clone, but requests to your new endpoint return 404.

Cause: the routes file was not registered in src/express/routes.ts (backend) or src/react/routes.tsx (frontend).

Solution:

// src/express/routes.ts
import postRoutes from "./modules/post/postRoutes";

router.use(postRoutes);

// src/react/routes.tsx
import { postRoutes } from "./components/post";
// ... add postRoutes to the children array

Important

make:clone creates the files but does not register them automatically. You must add the import yourself.

401 Unauthorized on POST / PUT / DELETE requests

Symptom: your GET requests work, but any mutation (POST, PUT, PATCH, DELETE) returns 401.

Cause: the CSRF token is missing. StartER uses a Client-Side Double-Submit pattern: every mutation request must include both a cookie and a matching header.

Solution for the browser: this is handled automatically by the useMutate() hook in src/react/helpers/mutate.ts. If you're making requests manually (e.g., with fetch), use the useMutate hook instead.

Solution for Postman / Insomnia:

  1. Add a header: x-csrf-token: test-token
  2. Add a cookie: __Host-x-csrf-token=test-token

The values must match. Their content doesn't matter for testing.

req.item is undefined (param converter not registered)

Symptom: your action crashes with Cannot read properties of undefined when accessing req.item (or req.post, etc.).

Cause: the param converter is not registered in the routes file.

Solution: make sure your routes file calls router.param():

// src/express/modules/post/postRoutes.ts
import postParamConverter from "./postParamConverter";

router.param("postId", postParamConverter.convert);

Without this line, Express doesn't know how to convert the :postId URL parameter into a loaded entity.

Schema mismatch after adding a column

Symptom: after adding a column to schema.sql, your API returns data without the new field, or TypeScript shows type errors.

Cause: you updated schema.sql but forgot one or more of these steps:

  1. Run npm run database:reset to recreate the database
  2. Update the type in src/types/index.d.ts
  3. Update the Zod schema in the repository
  4. Update the SQL queries in the repository (SELECT, INSERT, UPDATE)

Solution: follow the full checklist (see First exercises, Exercise 4). After modifying the type in index.d.ts, let TypeScript guide you: the compiler will flag every file that needs updating.

Tests fail after modifying a resource

Symptom: npm run test fails after adding or modifying a field on a resource.

Cause: the test fixtures and contracts don't know about the new field.

Solution: update both:

  1. tests/fixtures/items.ts : add the new field to each fixture object
  2. tests/contracts/items.ts : add the new field to the body of each mutation request (add, edit)

See First exercises, Exercise 5 for a step-by-step walkthrough.

npm run database:reset doesn't seem to work

Symptom: your schema changes don't take effect after running database:reset.

Possible causes:

  1. Syntax error in SQL: check your schema.sql for typos. SQLite will silently stop at the first error.
  2. Server still running: some operating systems lock the database file. Stop the dev server, run database:reset, then restart.
  3. Wrong database path: ensure you're running the command from the project root.

JWT / Authentication errors in development

Symptom: after restarting the server, you get 401 on previously authenticated requests.

Cause: the APP_SECRET was regenerated (or not set).

Solution: set a persistent APP_SECRET in your .env file:

APP_SECRET=your-development-secret-here

This ensures JWTs signed before a restart remain valid. Restart the server for the change to take effect.

SMTP_URL error on startup in production

Symptom: the server refuses to start and logs an error about SMTP_URL.

Cause: in production (NODE_ENV=production), StartER requires SMTP_URL to be defined. This is a safety check to ensure your application can actually send emails to users.

Solution: set SMTP_URL in your production environment:

SMTP_URL=smtp://user:password@smtp.example.com:587

In development, if SMTP_URL is not set, the magic link is printed to the console instead.

ReferenceError: window is not defined (or document / localStorage)

Symptom: the server crashes on initial request with ReferenceError: window is not defined during SSR.

Cause: accessing browser-only globals at the component top-level or outside useEffect.

Solution: wrap browser-only logic in useEffect (which runs exclusively on the browser) or guard with a type check:

// Good: runs only on client browser
useEffect(() => {
  const theme = localStorage.getItem("theme");
}, []);

// Alternative guard:
if (typeof window !== "undefined") {
  // Client-only execution
}

React Hydration Mismatch (Text content does not match server-rendered HTML)

Symptom: browser console warning stating server-rendered HTML does not match client DOM.

Cause: rendering dynamic values (e.g. new Date().toLocaleTimeString() or Math.random()) that produce different output on Node server vs client browser.

Solution: use stable initial props or format dates using StartER's datetime.ts helper with VITE_TIMEZONE.

SSR Network Fetch Errors (Absolute URL required)

Symptom: server-side loader fails with TypeError: Only absolute URLs are supported.

Cause: making raw fetch("/api/items") calls during server rendering without forwarding headers or base URL context.

Solution: use StartER's built-in getOrFetch(url) helper or forward request headers in loaders as shown in src/react/routes.tsx.

Best practices

  • Start by reading the error response message and HTTP status code before inspecting the codebase.
  • Terminal logs are often more detailed than the browser console, as the dev server prints full stack traces.
  • Treat TypeScript errors as your task list when modifying shared types or interfaces.

See also

Clone this wiki locally