Setting up the project you will build all course
One project, built across the whole course. Set it up properly now and you will not fight it later.
Do this at the keyboard. Everything from here assumes it exists.
What you need
- Node.js 20 or newer —
node --version - Docker for PostgreSQL, or a local Postgres if you prefer
- Git
- VS Code, with the ESLint and Prisma extensions
Node version managers are worth having. nvm on macOS and Linux, fnm on any
platform — projects pin different Node versions and switching by hand is
tedious.
The layout
kirana-store/
apps/
web/ Next.js front end
api/ NestJS back end
packages/
shared/ types both halves use
docker-compose.yml
package.json
.gitignore
README.md
This is a monorepo: several applications in one repository.
The alternative is two repositories, which is also common. A monorepo earns its
place here for one reason: packages/shared holds the types both halves use, so
a change to an API response shape becomes a compile error in the front end
immediately rather than a runtime surprise later. That is module 9's subject and
the main reason for the structure.
The cost is a slightly more complex setup, which is this lesson.
Creating it
mkdir kirana-store && cd kirana-store
git init
npm init -y
Make it a workspace root by editing package.json:
{
"name": "kirana-store",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev:web": "npm run dev --workspace=apps/web",
"dev:api": "npm run start:dev --workspace=apps/api"
}
}
private: true prevents publishing to npm by accident. workspaces tells npm
these folders are linked packages, so packages/shared can be imported by both
apps without publishing anything.
The database
# docker-compose.yml
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: kirana
POSTGRES_PASSWORD: localdev
POSTGRES_DB: kirana
ports:
- "5432:5432"
volumes:
- kirana-data:/var/lib/postgresql/data
volumes:
kirana-data:
docker compose up -d
docker compose ps
Two things to notice.
The named volume means your data survives docker compose down. Without it,
stopping the container deletes the database — which is occasionally what you
want and usually a nasty surprise.
localdev as a password is fine here and nowhere else. This container is
not reachable from outside your machine. Production credentials live in
environment variables and never in a file you commit.
The front end
npx create-next-app@latest apps/web
Answer: TypeScript yes, ESLint yes, Tailwind yes, src/ directory
yes, App Router yes, Turbopack yes, import alias default.
The App Router is not optional for this course — modules 3 and 4 assume it.
npm run dev:web
Open http://localhost:3000.
The back end
npm i -g @nestjs/cli
nest new apps/api --skip-git --package-manager npm
--skip-git because the repository already exists.
NestJS defaults to port 3000, which Next.js has. Change it:
// apps/api/src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix("api");
await app.listen(process.env.PORT ?? 3001);
}
setGlobalPrefix("api") puts every route under /api, which keeps things tidy
and makes proxying straightforward later.
npm run dev:api
http://localhost:3001/api should respond.
The shared package
mkdir -p packages/shared/src
{
"name": "@kirana/shared",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
// packages/shared/src/index.ts
export type Money = number; // always paise, never rupees
export interface Product {
id: string;
slug: string;
name: string;
pricePaise: Money;
unit: string;
inStock: boolean;
}
Note pricePaise. Module 2 of the Python course made the case and it applies
identically in JavaScript — worse, in fact, since every JavaScript number is a
float. 0.1 + 0.2 is 0.30000000000000004 here too.
Money is an integer count of paise, everywhere, converted only for display. The field name carries the unit so nobody has to guess. Deciding this on day one avoids a painful migration later.
npm install @kirana/shared --workspace=apps/web
npm install @kirana/shared --workspace=apps/api
Environment files
# apps/web/.env.local
NEXT_PUBLIC_API_URL=http://localhost:3001/api
# apps/api/.env
DATABASE_URL=postgresql://kirana:localdev@localhost:5432/kirana
PORT=3001
The API URL is NEXT_PUBLIC_ deliberately — the browser needs it and it is not
a secret. DATABASE_URL has no prefix and never will.
Commit examples, not values:
cp apps/web/.env.local apps/web/.env.example
cp apps/api/.env apps/api/.env.example
Then blank the values in the examples. A new developer sees which variables exist without receiving your credentials.
.gitignore
node_modules/
.next/
dist/
.env
.env.local
!.env.example
*.log
.DS_Store
The !.env.example line re-includes the examples after excluding .env*. Check
it works:
git status --porcelain | grep env
You should see the examples and not the real files. Verify this before your first commit — a secret committed once stays in history even after deletion, and the only real fix is rotating it.
First commit
git add -A
git commit -m "Set up monorepo with Next.js, NestJS and Postgres"
Check everything works
Three terminals, or three VS Code panes:
docker compose up -d
npm run dev:api
npm run dev:web
http://localhost:3000— Next.jshttp://localhost:3001/api— NestJSdocker compose ps— database healthy
All three green, and you are set up.
When it does not work
Port already in use. Something else is on 3000 or 3001. Find it with
lsof -i :3000 on macOS or Linux, or change the port.
Docker cannot bind 5432. A PostgreSQL is already running locally. Stop it,
or map to "5433:5432" and update DATABASE_URL.
Cannot find module '@kirana/shared'. Run npm install at the repository
root — workspaces link on install, not on creation.
TypeScript cannot resolve the shared package. Restart the TS server in VS
Code: command palette, TypeScript: Restart TS Server. It caches aggressively.
Check your work
Why a monorepo here: packages/shared holds the types both halves use, so a
change to an API shape becomes a compile error in the front end immediately.
Why pricePaise: every JavaScript number is a float, so 0.1 + 0.2 is
wrong here exactly as in Python. Money is an integer count of paise, converted
only for display, and the field name carries the unit.
Why the named Docker volume: without it, docker compose down deletes your
database.
Why NEXT_PUBLIC_ on the API URL but not the database URL: the browser
needs the API URL and it is not secret; the database URL must never leave the
server.
Why .env.example is committed: a new developer learns which variables
exist without receiving your credentials.
Cannot find module '@kirana/shared': run npm install at the repository
root — workspaces link on install.
How many places reference the API port: the API's .env, the web app's
.env.local, and CORS configuration. If that is more than you would like, that
is a sign to derive it from one value.
Practice
- Complete the setup. Get all three running at once.
- Add a field to the
Producttype insharedand import it in both apps. Confirm both see it. - Confirm
.envfiles are ignored and.env.examplefiles are not. - Stop the database with
docker compose down, restart it, and confirm data would survive — check the volume still exists withdocker volume ls. - Change the API port to 3002 and update everything that refers to it. Notice how many places that is, and consider whether it should be fewer.
- Write the README: prerequisites, setup steps, how to run each part.
- Push to GitHub and check the repository contains no
.envand nonode_modules.
That is module one. You have the model and the project.
Next module: React — components, state, and the parts people misuse.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship