SkillShare Logo
STEP 3 OF 7 · DATA

Database & Models

Spin up your database in Docker, then describe the two things this app stores: users and products. Use the Postgres tab (TypeORM) or the MongoDB tab (Mongoose) — whichever you picked.

01 — CHOOSE

Postgres vs MongoDB

Relational tables (Postgres) or flexible documents (MongoDB). Either works fine for this app — pick one and follow its tab everywhere.

PostgreSQL + TypeORMPOSTGRESQL + TYPEORM

  • Structured tables with strict types
  • Great for clear relationships & reporting
  • TypeORM models tables as decorated classes
  • First-class NestJS integration

MongoDB + MongooseMONGODB + MONGOOSE

  • Flexible JSON-like documents
  • Fast to start, schema can evolve
  • Mongoose adds schemas & validation
  • Feels natural if you think in JSON
Unsure?
Pick PostgreSQL. Inventory data is naturally tabular, and TypeORM gives you type safety end-to-end.
02 — RUN IT

Set Up the Database

No messy local install — Postgres runs in a local Docker container, and MongoDB runs in the cloud via MongoDB Atlas.

Create a docker-compose.yml file in your database directory (or project root) to spin up the container. To keep sensitive credentials secure and out of version control, we use environment variables that reference a local .env file:

docker-compose.yml
version: "3.8"

services:
  db:
    image: postgres:16
    container_name: inv-pg
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Run the compose file in detached mode:

terminal
$ docker compose up -d

Then define your connection string in the backend's .env file:

server/.env
# server/.env →
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secret
POSTGRES_DB=inventory
DATABASE_URL="postgresql://postgres:secret@localhost:5432/inventory"
Daily commands
For Postgres, use docker compose start / docker compose stop (or docker compose down) to manage your container. MongoDB Atlas is hosted in the cloud, so it is always online and doesn't require daily commands. In the final step, we'll expand our Docker Compose setup to run the entire backend and frontend stack together.
03 — THE SHAPE

The Data Model

Two tables/collections power everything. Keep them small — you can always add fields later.

U

User

_id · name · email (unique) · password (hashed) · isVerified (default false) · verificationToken · createdAt · updatedAt

P

Product

_id · name · description · sku (unique, uppercase) · category · price · quantity · minStockLevel · createdBy (ref User) · createdAt · updatedAt

Why isVerified and minStockLevel?
isVerified blocks login until the email link is clicked. minStockLevel is the threshold that triggers a low-stock alert when quantity drops to or below it. createdBy links each product to the user who owns it, so users only see their own inventory.
04 — DEFINE

Connect & Define the Models

Pick your database tab. Each shows the connection plus both Express and NestJS wiring.

TypeScript & Decorators (NestJS only)
NestJS is built on TypeScript and uses decorators. For JavaScript + Express, we define our database schemas using standard EntitySchema definitions, which require no decorator configuration or build pipeline.

Express Project Structure

Project Structure
server/
├── src/
│   ├── config/
│   │   └── db.js          # TypeORM DataSource instance (db connection)
│   ├── models/
│   │   ├── User.js        # User database entity schema
│   │   └── Product.js     # Product database entity schema
│   └── app.js             # App entry file (initializes DataSource)
└── server.js              # Server entry point

Entities

server/src/models/User.js
import { EntitySchema } from "typeorm";

export const User = new EntitySchema({
  name: "User",
  tableName: "user",
  columns: {
    id: { primary: true, type: "uuid", generated: "uuid" },
    name: { type: "varchar" },
    email: { type: "varchar", unique: true },
    passwordHash: { type: "varchar" },
    isVerified: { type: "boolean", default: false },
    verificationToken: { type: "varchar", nullable: true },
    verificationTokenExpiry: { type: "timestamp", nullable: true },
    createdAt: { type: "timestamp", createDate: true },
  },
});
server/src/models/Product.js
import { EntitySchema } from "typeorm";

export const Product = new EntitySchema({
  name: "Product",
  tableName: "product",
  columns: {
    id: { primary: true, type: "uuid", generated: "uuid" },
    name: { type: "varchar" },
    description: { type: "varchar", nullable: true, default: "" },
    sku: { type: "varchar", unique: true },
    category: { type: "varchar" },
    price: { type: "float", default: 0 },
    quantity: { type: "int", default: 0 },
    minStockLevel: { type: "int", default: 10 },
    createdBy: { type: "uuid" },
    createdAt: { type: "timestamp", createDate: true },
    updatedAt: { type: "timestamp", updateDate: true },
  },
});

Database Configuration

server/src/config/db.js
import { DataSource } from "typeorm";
import { User } from "../models/User.js";
import { Product } from "../models/Product.js";

export const AppDataSource = new DataSource({
  type: "postgres",
  url: process.env.DATABASE_URL,
  entities: [User, Product],
  synchronize: true, // auto-creates tables in dev — turn OFF in prod
});

// in app.js, before app.listen():
await AppDataSource.initialize();
About synchronize / auto-schema
In development, letting the ORM (the library that maps code to database tables) create tables for you is convenient. For production, generate proper migrationsinstead so you never risk data loss. See the TypeORM & Mongoose docs linked above.
05 — CHECKPOINT

Confirm the Connection

Restart your backend. On boot you should see the connection log and (for Postgres) two tables created.

  • Database is running (Docker container for Postgres, cloud Atlas cluster for MongoDB)
  • Backend logs "connected" with no errors on start
  • User and Product models are defined and imported