-
Notifications
You must be signed in to change notification settings - Fork 7
Troubleshooting
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
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): Pure database access & runtime Zod schema verification (z.ZodType<Item>). Repositories know nothing about HTTP requests or routes and can be unit tested in isolation. -
Validators (
itemValidator.ts): Sanitize and validate untrusted client inputs before they reach controller logic. -
Actions (
itemActions.ts): Lean controller functions (~5 lines of code) focused strictly on HTTP response status codes and 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:
-
Explicit Route Registration: Automatic AST/regex route patching scripts are fragile and hide application control flow. Requiring explicit imports in
src/express/routes.tsandsrc/react/routes.tsxensures you always know how routes are wired. -
Code vs. Intention:
make:cloneduplicates 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.
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:
-
schema.safeParse(req.body)validates only untrusted fields submitted by the client (e.g.{ title: "My item" }). -
inject(req)injects trusted server-side context (e.g.{ user_id: req.me.id }). -
req.bodyis 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/awaitoverhead, unhandled promise rejections, and ORM query magic. - Data access methods read like pure, synchronous TypeScript functions.
- Prevents
BigIntserialization crashes by coercing SQLite IDs to standard numbers (type RowId = number).
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.
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 arrayImportant
make:clone creates the files but does not register them automatically. You must add the import yourself.
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:
- Add a header:
x-csrf-token: test-token - Add a cookie:
__Host-x-csrf-token=test-token
The values must match. Their content doesn't matter for testing.
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.
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:
- Run
npm run database:resetto recreate the database - Update the type in
src/types/index.d.ts - Update the Zod schema in the repository
- 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.
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:
-
tests/fixtures/items.ts: add the new field to each fixture object -
tests/contracts/items.ts: add the new field to thebodyof each mutation request (add,edit)
See First exercises, Exercise 5 for a step-by-step walkthrough.
Symptom: your schema changes don't take effect after running database:reset.
Possible causes:
-
Syntax error in SQL: check your
schema.sqlfor typos. SQLite will silently stop at the first error. -
Server still running: some operating systems lock the database file. Stop the dev server, run
database:reset, then restart. - Wrong database path: ensure you're running the command from the project root.
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-hereThis ensures JWTs signed before a restart remain valid. Restart the server for the change to take effect.
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:587In development, if SMTP_URL is not set, the magic link is printed to the console instead.
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
}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.
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.
- Read the error message first: StartER's error responses include the HTTP status code and a descriptive message. Start there before searching the code.
- Check the terminal: the dev server logs all errors with stack traces. The browser console only shows the HTTP status.
- Use TypeScript as your checklist: after modifying a shared type, fix every TypeScript error before running the app. They are your TODO list.
AI co-creation
Getting started
Explanations
How-To Guides
Reference
Digging deeper