RizTech Academy logo
RizTech Academy
How a Full-Stack Application Fits TogetherLesson 4 of 430 min

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.js
  • http://localhost:3001/api — NestJS
  • docker 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

  1. Complete the setup. Get all three running at once.
  2. Add a field to the Product type in shared and import it in both apps. Confirm both see it.
  3. Confirm .env files are ignored and .env.example files are not.
  4. Stop the database with docker compose down, restart it, and confirm data would survive — check the volume still exists with docker volume ls.
  5. 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.
  6. Write the README: prerequisites, setup steps, how to run each part.
  7. Push to GitHub and check the repository contains no .env and no node_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