Express + TypeScript + Prisma + PostgreSQL + Redis.
This repo mirrors the layout and conventions of our production service. A
working approval CRUD module is included as the reference implementation —
get it running, read it, then build your task in the same shape.
Budget about 10 minutes for setup. If you get stuck on setup specifically, message your interviewer rather than burning test time on it.
| Tool | Version | Check with |
|---|---|---|
| Node.js | 20 LTS (18.18+ ok) | node -v |
| npm | 9+ | npm -v |
| PostgreSQL | 14+ | psql --version |
| Redis | 6+ | redis-server -v (memurai --version on native Windows) |
You do not need Postgres or Redis installed locally if you have Docker — see Option A below.
Step 4 has a route for every OS: Docker (any), macOS, Linux, Windows via WSL2, and native Windows.
Windows: prefer Docker Desktop or WSL2 over the native install — Redis has
no official Windows build, so the native route needs a Redis-compatible
substitute. Whichever you pick, run the npm commands from PowerShell (or a
WSL terminal), not the legacy cmd prompt.
npm installcp .env.example .envOn Windows PowerShell, cp is an alias for Copy-Item, so the same line works.
In cmd it's copy .env.example .env.
The defaults in .env.example match the Docker setup in step 4, so if you go
the Docker route you can leave the file as-is. If you were given a hosted
database, paste that connection string into DATABASE_URL instead.
| Variable | What it is |
|---|---|
PORT |
HTTP port, defaults to 3000 |
DATABASE_URL |
Postgres connection string |
REDIS_URL |
Redis connection string |
DEFAULT_CLIENT_ID |
Tenant used when a request sends no client-id header |
LOG_LEVEL |
info by default, set debug if you want noisier logs |
DEFAULT_CLIENT_ID must match the client_id written by the seed script —
don't change it unless you also change prisma/seed.ts.
Works identically on macOS, Windows, and Linux. On Windows you need Docker Desktop with the WSL2 backend enabled (its default).
docker compose up -dThat's Postgres on 5432 and Redis on 6379, matching .env.example exactly —
no .env edits needed. Confirm both are up:
docker compose psStop them later with docker compose down, or docker compose down -v to also
wipe the data.
brew install postgresql@16 redis && brew services start postgresql@16 && brew services start rediscreatedb backend_testHomebrew Postgres has no password and uses your own username, so set:
DATABASE_URL="postgresql://YOUR_MAC_USERNAME@localhost:5432/backend_test?schema=public"
If brew services start redis reports an error, see the Redis 8 note in
Troubleshooting — it's a known Homebrew config bug with a one-line fix.
Debian / Ubuntu:
sudo apt update && sudo apt install -y postgresql redis-serverFedora / RHEL:
sudo dnf install -y postgresql-server postgresql-contrib redis && sudo postgresql-setup --initdbArch:
sudo pacman -S postgresql redis && sudo -u postgres initdb -D /var/lib/postgres/dataStart both, and have them come back after a reboot:
sudo systemctl enable --now postgresql redisOn Debian/Ubuntu the Redis unit is named redis-server, so use
sudo systemctl enable --now postgresql redis-server there.
Postgres on Linux ships with peer authentication and only a postgres
superuser, so give your own account a role and a database:
sudo -u postgres createuser --superuser $USERcreatedb backend_testDATABASE_URL="postgresql://YOUR_LINUX_USERNAME@localhost:5432/backend_test?schema=public"
Note that peer auth works over the Unix socket but the connection string above
goes over TCP to localhost. If that gets rejected, either add a password to
your role (sudo -u postgres psql -c "ALTER ROLE $USER WITH PASSWORD 'devpass';"
and put it in the URL) or set the localhost lines in pg_hba.conf to trust
for local development.
Distro Node packages are often too old for Prisma 6 — check node -v and
install 20 LTS via nvm or NodeSource if you're
below 18.18.
The closest thing to how this runs in production, and everything below is just the Linux path.
In PowerShell as Administrator, once:
wsl --installReboot, let Ubuntu finish its first-run setup, then do everything else inside the Ubuntu terminal:
sudo apt update && sudo apt install -y postgresql redis-serversudo service postgresql start && sudo service redis-server startThese services do not auto-start when you open a new WSL terminal — re-run
that service line each session, or you'll get connection-refused errors.
Give your Linux user a Postgres role and a database:
sudo -u postgres createuser --superuser $USER && createdb backend_testDATABASE_URL="postgresql://YOUR_WSL_USERNAME@localhost:5432/backend_test?schema=public"
Two things that will bite you if you skip them:
- Keep the repo inside the WSL filesystem (
~/backend-test), not under/mnt/c/.... On the Windows mount,nodemonmisses file changes andnpm installis several times slower. - Install Node inside WSL (nvm is easiest) rather than relying on a Windows
Node install — a Windows
node.exeon the WSL path produces confusing native-module errors.
localhost:3000 in your Windows browser reaches the server running in WSL, so
Postman and your browser work normally.
Postgres — install from
postgresql.org/download/windows
(or winget install PostgreSQL.PostgreSQL.16). During setup:
- note the password you set for the
postgresuser — you need it below - keep the default port
5432 - tick the option to add the tools to
PATH; if you miss it, addC:\Program Files\PostgreSQL\16\binto your PATH manually orpsqlwon't be found
Create the database:
createdb -U postgres backend_testThen set, with your own password URL-encoded if it contains @ : / ? etc:
DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@localhost:5432/backend_test?schema=public"
Redis — there is no official Redis build for Windows. Use Memurai
Developer Edition, a free, Redis-compatible Windows service:
memurai.com. It listens on 6379 and
installs as an auto-starting Windows service, so REDIS_URL stays as-is and
the app cannot tell the difference.
Do not use the old redis-windows / MicrosoftArchive builds — they are Redis
3.x, unmaintained, and this project's client speaks a newer protocol.
macOS / Linux / WSL:
pg_isready && redis-cli pingWindows PowerShell (native install):
pg_isready; memurai-cli pingYou want accepting connections and PONG.
npx prisma migrate deploy && npx prisma generate && npm run seedmigrate deployapplies the committed migration inprisma/migrations/generatebuilds the typed Prisma client intonode_modulesseedinserts two customers and two leads to work against
The seed prints the ids you'll need:
Seed complete
client-id: 11111111-1111-1111-1111-111111111111
lead-id (Aditi Sharma): 33333333-3333-3333-3333-333333333333
lead-id (Rahul Verma): 55555555-5555-5555-5555-555555555555
The seed is idempotent — re-run it any time.
If you change prisma/schema.prisma later, use npx prisma migrate dev --name <what-you-changed> instead. It creates a new migration, applies it, and
regenerates the client. Commit the generated migration file.
npm run devYou should see:
Redis TCP connection established
Redis ready
Redis connected!
Backend test server is live at port 3000
The server refuses to start if Redis is unreachable — that's deliberate, not a
bug. nodemon restarts it on every file save.
Windows PowerShell:
curlis an alias forInvoke-WebRequest, which does not accept-X,-H, or-dand will throw a parameter error on the commands below. Either callcurl.exeexplicitly (it ships with Windows 10+ and behaves like real curl), or use theInvoke-RestMethodversions given after each example. PowerShell also doesn't understand the\line continuations — put each command on one line, or swap\for a backtick.
Health check:
curl http://localhost:3000/healthExpected: {"message":"ok"}
Now run the reference CRUD against a seeded lead:
curl -X POST http://localhost:3000/crm-api/approval/add/33333333-3333-3333-3333-333333333333 \
-H 'Content-Type: application/json' \
-d '{"branch":"Delhi","approvalAmount":50000,"roi":0.1,"monthlyIncome":45000,"salaryDate":"01","repayDate":"05-09-2026","processingFeePercent":3,"processingFee":1500,"gst":18,"email":"aditi@work.com","alternateNumber":"9999911111","cibilScore":740,"loanPurpose":"Medical","status":"Pending","remark":"looks fine"}'The same thing in PowerShell:
$body = @{ branch='Delhi'; approvalAmount=50000; roi=0.1; monthlyIncome=45000; salaryDate='01'; repayDate='05-09-2026'; processingFeePercent=3; processingFee=1500; gst=18; email='aditi@work.com'; alternateNumber='9999911111'; cibilScore=740; loanPurpose='Medical'; status='Pending'; remark='looks fine' } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://localhost:3000/crm-api/approval/add/33333333-3333-3333-3333-333333333333 -ContentType 'application/json' -Body $bodyExpected: {"message":"Approval added"}
curl http://localhost:3000/crm-api/approval/get/33333333-3333-3333-3333-333333333333Invoke-RestMethod http://localhost:3000/crm-api/approval/get/33333333-3333-3333-3333-333333333333 | ConvertTo-Json -Depth 5Expected: an approvalData object with "loanNo":"LN000001" and
"disbursalAmount":48230.
If both of those worked, your environment is good. Delete the row again with
DELETE /crm-api/approval/delete/<approvalId> if you want a clean slate.
Want to see the cache? After a GET, run redis-cli keys 'approval:*' — the
key is there, and it disappears after any write. On a native Windows install
that's memurai-cli keys "approval:*".
npm run validateThat runs the TypeScript compiler and ESLint. It must pass.
| Symptom | Fix |
|---|---|
ECONNREFUSED ... 5432 |
Postgres isn't running. docker compose up -d, or start your local service. |
ECONNREFUSED ... 6379 and the server exits |
Redis isn't running. Same fix. |
P1000: Authentication failed |
Wrong user/password in DATABASE_URL. On Homebrew Postgres it's your own username, no password. |
P1003: Database does not exist |
createdb backend_test, or let Docker create it. |
P3005: The database schema is not empty |
You pointed at a used database. Use a fresh one, or npx prisma migrate reset to wipe it. |
EADDRINUSE :::3000 |
Something else owns port 3000. Change PORT in .env. |
Cannot find module '.prisma/client' / stale model types |
npx prisma generate. |
function gen_random_uuid() does not exist |
Postgres older than 13. Upgrade, or CREATE EXTENSION pgcrypto; in the database. |
| Seed says a row already exists | Harmless — the seed upserts. Nothing to do. |
| Changed the schema and everything is confused | npx prisma migrate reset && npm run seed starts over. |
| Symptom | Fix |
|---|---|
brew services start redis shows error, and redis.log says Can't load module from ./modules/redisbloom/redisbloom.so |
Homebrew's Redis 8 config lists modules it never installs, with relative paths that don't resolve. Comment out the four loadmodule ./modules/... lines in /opt/homebrew/etc/redis.conf, then brew services restart redis. Nothing here uses those modules. |
psql: command not found after brew install postgresql@16 |
It's keg-only. brew link postgresql@16 --force, or add /opt/homebrew/opt/postgresql@16/bin to your PATH. |
| Symptom | Fix |
|---|---|
P1000 / peer authentication failed for user "you" |
You have no Postgres role. sudo -u postgres createuser --superuser $USER, then createdb backend_test. |
password authentication failed over localhost even though psql works |
psql uses the Unix socket, Prisma uses TCP. Give the role a password and put it in DATABASE_URL, or set the 127.0.0.1/::1 lines in pg_hba.conf to trust and sudo systemctl reload postgresql. |
Unit redis.service not found on Debian/Ubuntu |
The unit is redis-server: sudo systemctl enable --now redis-server. |
initdb-related errors on Fedora/Arch |
The data directory was never created. Re-run the postgresql-setup --initdb / initdb -D line from Option C. |
Prisma complains about the Node version, or npm install fails oddly |
Your distro's Node is too old. Install 20 LTS with nvm and re-run npm install. |
| Services gone after reboot | You started them without enable. sudo systemctl enable --now postgresql redis-server. |
| Symptom | Fix |
|---|---|
Invoke-WebRequest: A parameter cannot be found that matches parameter name 'X' |
PowerShell aliases curl. Use curl.exe ... or the Invoke-RestMethod form in step 7. |
psql/createdb not recognised |
Postgres' bin folder isn't on PATH. Add C:\Program Files\PostgreSQL\16\bin and open a new terminal. |
P1000: Authentication failed for user "postgres" |
Wrong password in DATABASE_URL, or it contains characters needing URL-encoding (@ → %40, : → %3A, / → %2F, # → %23). |
| Redis connection refused, native install | Memurai isn't running. Get-Service Memurai to check, Start-Service Memurai to start it. |
| WSL: connection refused after reopening the terminal | WSL doesn't persist services. Re-run sudo service postgresql start && sudo service redis-server start. |
WSL: file saves don't trigger a nodemon restart |
The repo is on /mnt/c. Move it into the WSL filesystem, e.g. ~/backend-test. |
npm install errors about native modules / node_modules behaves oddly under WSL |
You're using the Windows node.exe from inside WSL. Install Node inside WSL (nvm) and which node should print a Linux path, not /mnt/c/.... |
EADDRINUSE :::3000 and you can't find the process |
netstat -ano | findstr :3000 to get the PID, then taskkill /PID <pid> /F. |
| Git shows every line changed after you edit a file | Line-ending churn. git config --global core.autocrlf input before you start. |
| Command | What it does |
|---|---|
npm run dev |
Start the server with hot reload |
npm run build |
Compile TypeScript to build/ |
npm start |
Run the compiled build |
npm run seed |
Insert the sample customers and leads |
npm run studio |
Open Prisma Studio to browse the database |
npm run type-check |
tsc --noEmit |
npm run lint |
ESLint |
npm run validate |
type-check + lint |
server.ts app bootstrap, middleware, graceful shutdown
crm-routes.ts mounts every feature router under /crm-api
prisma-client.ts shared PrismaClient
redis-client.ts shared Redis client + connect/disconnect
logger.ts winston JSON logger
load-env.ts must be imported first, before any local module
prisma/schema.prisma Postgres schema
prisma/seed.ts sample customers + leads
server/<feature>/
<feature>.routes.ts HTTP layer: params, validation, status codes
<feature>.service.ts business logic, caching, response shaping
<feature>.model.ts the only layer that talks to Prisma
<feature>-types.ts request/response types
server/middleware/ cross-cutting middleware
Conventions to follow:
- Every function takes a single named-argument object, never positional args.
- Routes never call Prisma directly — they go through the model (or the service).
- Each module exports one object:
export const approvalModel = { ... }. - Every model query is scoped by
client_id(the tenant). Never drop it. - Errors are caught per route, logged with
logger.error(error, { route, clientId }), and answered with a generic 500 — never leak a stack trace to the client. - Dates arriving over HTTP use
dd-MM-yyyyand are parsed withdate-fns.
There is none, on purpose. server/middleware/client.middleware.ts stands in
for the real JWT middleware: it reads the tenant from a client-id header and
falls back to DEFAULT_CLIENT_ID in .env. clientId still flows through
every layer exactly as it does in production.
approval.service.ts shows the expected caching pattern: a read-through cache
with a TTL, invalidated on every write. Cache failures are logged and swallowed
— a Redis problem must degrade to a database read, never fail the request.
Base path /crm-api/approval. Seeded ids:
client-id:11111111-1111-1111-1111-111111111111- lead:
33333333-3333-3333-3333-333333333333(Aditi Sharma) - lead:
55555555-5555-5555-5555-555555555555(Rahul Verma)
| Method | Path | Purpose |
|---|---|---|
POST |
/add/:leadId |
Create the approval for a lead (one per lead) |
GET |
/get/:leadId |
Read one approval (cached) |
GET |
/get-all |
List — ?limit=&offset=&status= |
PUT |
/update/:approvalId |
Partial edit — only the keys you send change |
DELETE |
/delete/:approvalId |
Delete and revert the lead's status |
Creating or editing an approval also moves the parent lead:
Approved → Approved, Pending → Review Approval, Rejected → Rejected.
Deleting one sends the lead back to Documents Received.
curl -X PUT http://localhost:3000/crm-api/approval/update/<approvalId> \
-H 'Content-Type: application/json' \
-d '{"approvalAmount":65000,"status":"Approved"}'curl -X DELETE http://localhost:3000/crm-api/approval/delete/<approvalId>Ask your interviewer which module to build. Whatever it is, we are looking for:
- The same four-file module structure, mounted in
crm-routes.ts. - Correct status codes —
400for bad input,404for missing rows,409for conflicts,500only for genuine failures. - Input validated at the route boundary before anything touches the database.
- Tenant scoping on every query.
- Cache invalidated on writes if you cache reads.
npm run validatepassing.
Commit as you go — we read the history.