What This Part Covers
- Installing every system dependency (Node, Yarn, Docker, VS Code)
- Creating the Nx monorepo workspace from scratch
- Adding NestJS backend + Next.js frontend + shared library
- Spinning up PostgreSQL, Redis, and Adminer via Docker
- Writing the environment file
- Booting the server and verifying it works
- Setting up a Conventional Commits workflow with GitHub Desktop and Commitizen
By the end of this part, you will have a running (empty) entodo backend accessible at http://localhost:3333/graphql, with a Conventional Commits workflow in place.
Meteor Equivalent
In Meteor you ran one command:
meteor create --blaze my-todo-app
cd my-todo-app
meteor
# → App running at http://localhost:3000
The framework created the directory structure, started MongoDB, and served both client and server in one process.
In the enterprise world, you build this infrastructure yourself — explicitly. It takes longer the first time. After that, every project starts the same way and every team member knows exactly what is running and why.
Architectural Foundations: The Three “-ilities”
Enterprise development separates itself from amateur work by prioritising the lifecycle of code over the speed of immediate delivery. Three structural pillars govern every decision in this project:
- Extensibility — the codebase must accept new modules and business requirements without fracturing or triggering breaking rewrites of core logic.
- Maintainability — code is read far more times than it is written. Names, boundaries, types, and documentation must make debugging and refactoring straightforward for anyone who opens a file.
- Reusability — common configurations, logic gates, schema types, and validations are written once to eliminate cross-system synchronisation bugs from copy-paste patterns.
The City District Model
The entodo monorepo is structured like a city district with zoning laws:
graph TD
subgraph Workspace [Nx Monorepo: entodo]
API[apps/api <br> NestJS Backend]
WEB[apps/web <br> Next.js Frontend]
CONTRACTS[libs/contracts <br> Shared Isomorphic Types]
end
API -->|Imports Types| CONTRACTS
WEB -->|Imports Types| CONTRACTS
WEB -.->|HTTP / GraphQL requests only| API
apps/api and apps/web are separate buildings in the district — each with its own zoning permit. They operate completely independently. libs/contracts is the public post office — the only legal channel for sharing constants: interfaces and validation types.
The Rule of Boundaries:
@nx/enforce-module-boundariesis the City Inspector. The moment code tries to bypass the post office and reach directly into another building’s private files, the linter fires at IDE level and blocks the CI/CD pipeline.
1. Machine Prerequisites
1.1 Node.js via nvm
Never install Node directly. Use nvm (Node Version Manager) — it lets you switch Node versions per project without conflicts.
# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Reload your shell (or open a new terminal)
source ~/.zshrc # or ~/.bashrc on Linux
# Install and use Node 20 (the project's LTS version)
nvm install 20
nvm use 20
nvm alias default 20
# Verify
node -v # v20.x.x
npm -v # 10.x.x
Why Node 20 specifically? NestJS 11 requires Node 18+. Node 22 is the latest but has occasional compatibility issues with some packages. Node 20 is the LTS version — stable, widely tested, and officially supported by NestJS.
1.2 Yarn 1.x
This project uses Yarn Classic (v1), not Yarn 2/3/4. Do not upgrade.
npm install -g yarn
yarn -v # 1.22.x
Why Yarn over npm? Yarn 1 has a deterministic lockfile (
yarn.lock) and faster installs via cache. Nx also has first-class Yarn support for workspace management. The “classic” version is deliberately chosen for stability — Yarn 2+ changed the resolution model significantly.
1.3 Docker Desktop
Download from docker.com/products/docker-desktop.
After installation:
docker -v # Docker version 24+
docker compose version # Docker Compose v2.x
What Docker gives you: Instead of installing PostgreSQL and Redis directly on your machine (which conflicts between projects), Docker runs each service in an isolated container. You can start, stop, and delete them without touching your OS. Every team member runs identical infrastructure.
1.4 Git
macOS ships with an old Git. Upgrade:
brew install git
git -v # git version 2.x
2. VS Code Setup
2.1 Install VS Code
Download from code.visualstudio.com.
2.2 Install Extensions
zsh: command not found: code? Thecodecommand isn’t in your PATH yet. Install it from VS Code:
- Open VS Code
- Press Cmd+Shift+P and run:
Shell Command: Install 'code' command in PATH- Restart your terminal
Then retry:
code --install-extension nrwl.angular-consoleAlternatively, install the extension directly in VS Code via Extensions (Cmd+Shift+X) → search “Angular Console” → Install.
Run these commands one by one (or paste all at once):
code --install-extension nrwl.angular-console # Nx Console — visual project graph + task runner
code --install-extension esbenp.prettier-vscode # Auto-format on save
code --install-extension dbaeumer.vscode-eslint # Inline lint errors
code --install-extension eamodio.gitlens # Git blame, history, CodeLens
code --install-extension kumar-harsh.graphql-for-vscode # GraphQL schema syntax + autocomplete
code --install-extension firsttris.vscode-jest-runner # Run single Jest test from editor
code --install-extension mikestead.dotenv # .env syntax highlighting
code --install-extension ms-azuretools.vscode-docker # Container management UI
code --install-extension Gruntfuggly.todo-tree # Shows TODO/FIXME in sidebar
2.3 VS Code User Settings
Open Cmd+Shift+P → “Open User Settings (JSON)” and add:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["javascript", "typescript"],
"typescript.tsdk": "node_modules/typescript/lib"
}
What each setting does:
formatOnSave— runs Prettier every time you save. Eliminates all whitespace/format debates.defaultFormatter— ensures VS Code uses Prettier, not its own formatter.source.fixAll.eslint— auto-fixes ESLint errors on save (unused imports, sorting).typescript.tsdk— uses the project’s TypeScript version, not VS Code’s bundled one.
3. Create the Nx Workspace
This is the enterprise equivalent of meteor create. One command creates the monorepo scaffold:
cd dev # any folder you prefer to host your app
npx create-nx-workspace@latest entodo --preset=apps
When prompted:
- CI/CD pipeline? → Skip (we configure this in Part 12)
- Enable Nx Cloud? → No (free tier but not needed for learning)
cd entodo
Your directory now looks like:
entodo/
├── apps/ ← your applications live here
├── libs/ ← shared libraries live here
├── nx.json ← Nx configuration
├── package.json ← root package.json (all deps managed here)
└── tsconfig.base.json ← shared TypeScript config
Meteor analogy: Meteor has one directory for everything. Nx separates apps (
apps/) from shared code (libs/). Theapps/directory is whereapps/api(your NestJS server) andapps/web(your Next.js frontend) will live.
3.1 Add Framework Plugins
Install the Nx plugins for NestJS, Next.js, and plain JS libraries:
yarn add --dev @nx/nest @nx/next @nx/js
These plugins give you generators (code scaffolding commands) that know how to create NestJS apps, Next.js apps, and shared libraries inside your monorepo.
3.2 Generate the NestJS Backend
npx nx g @nx/nest:app apps/api
When prompted (need to arrow up, not default):
- linter? →
eslint - Port? →
3333 - E2E test runner? →
jest
This creates apps/api/ with a minimal NestJS app. Check what was created:
apps/api/
├── src/
│ ├── app/
│ │ ├── app.module.ts ← root NestJS module
│ │ └── app.controller.ts ← default HTTP controller (we'll replace this)
│ └── main.ts ← entry point, bootstraps the app
├── project.json ← Nx target definitions (build, serve, test, lint)
└── tsconfig.app.json ← TypeScript config for this app
Meteor analogy:
apps/api/src/main.tsis yourserver/main.js.app.module.tsis the root of everything the server knows about — equivalent to all yourserver/imports combined.
Ribbon-cutting:
main.tsis the ribbon-cutting ceremony. Before any wing (module) can open for business, the ceremony unlocks the doors (NestFactory.create), registers global pipes, validates the.envpolicy handbook (ConfigModule), and opens reception. Once the ribbon is cut, requests can start arriving.
3.3 Generate the Next.js Frontend
npx nx g @nx/next:app apps/web --src=true --appDir=true --style=css
When prompted:
- linter? →
eslint - unit test runner? →
jest(we use Jest for unit tests) - E2E test runner? →
playwrightornone
@nx/next does not have a setup-tailwind generator — add Tailwind manually. This project uses Tailwind CSS v4, which uses a CSS-first configuration approach (no tailwind.config.js, no init command):
yarn add --dev tailwindcss postcss autoprefixer @tailwindcss/postcss
Create postcss.config.js in the workspace root:
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};
Replace the contents of apps/web/src/app/global.css with:
@import "tailwindcss";
global.cssnotglobals.css: Nx generates the file asglobal.css(no trailings). The import inlayout.tsxalready points to the correct name — don’t rename it.
Tailwind v4 vs v3: V4 removes
tailwind.config.jsand the three@tailwinddirectives. Source file detection is automatic. Custom theme extensions use@themeblocks directly in CSS instead of a config file.
Finally, replace the Nx placeholder apps/web/src/app/page.tsx with a clean starter:
export default function Index() {
return (
<main className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900">entodo</h1>
<p className="mt-2 text-gray-500">Frontend is up and running.</p>
</div>
</main>
);
}
Why replace the default page? The Nx-generated
page.tsxreferences custom CSS class names (.wrapper,.container,.button-pill) that depended on styles Nx would have bundled with a different template. Those styles are gone once you adopt Tailwind v4. Tailwind’s preflight also resets SVG elements todisplay: block, causing any unsized SVG to expand and fill its container. The placeholder page is never meant to be kept — replace it immediately with your own markup.
This gives you apps/web/ with a Next.js App Router app and Tailwind CSS v4 confirmed working.
Meteor analogy:
apps/web/is yourclient/directory. But now it is a completely separate application — it communicates withapps/apionly through HTTP, not through shared memory.
3.4 Generate the Shared Contracts Library
npx nx g @nx/js:lib libs/contracts --bundler=tsc
- linter? →
eslint - unit test runner? →
jest
This creates a shared TypeScript library. Both apps/api and apps/web can import from it.
libs/contracts/
└── src/
├── index.ts ← exports everything
└── lib/
└── contracts.ts ← your shared types
Meteor analogy: In Meteor you put isomorphic code in
imports/and both client and server could import it. In the enterprise monorepo,libs/contractsis that shared space — but it exports only what you explicitly export, and only TypeScript types (no server code on the client, no client code on the server).
The city district: The full monorepo is a city district with zoning laws.
apps/apiandapps/webare separate buildings — each with its own zoning permit.libs/contractsis the public post office — the only legal way for buildings to share information. The City Inspector (@nx/enforce-module-boundaries) blocks any illegal cross-zone import at IDE level, before CI even runs.
Memory hook: Nx = city district. apps/ = separate buildings. libs/ = the public post office. @nx/enforce-module-boundaries = the City Inspector. Direct imports between apps = zoning violation — lint error before CI even runs.
4. Install Backend Dependencies
From the workspace root:
yarn add @nestjs/graphql @nestjs/apollo graphql@16 @apollo/server @as-integrations/express5 express
yarn add @nestjs/typeorm typeorm pg
yarn add @nestjs/cqrs nestjs-typed-cqrs nestjs-dev-utilities
yarn add @ptc-org/nestjs-query-graphql @ptc-org/nestjs-query-typeorm @ptc-org/nestjs-query-core
yarn add @nestjs/config
yarn add @nestjs/passport passport passport-jwt
yarn add @nestjs/jwt
yarn add class-validator class-transformer
yarn add bcrypt
yarn add --dev @types/pg @types/passport-jwt @types/bcrypt
yarn add helmet
reflect-metadatagotcha: After installing, check thatreflect-metadatais^0.2.2in your rootpackage.json.typeormandnestjs-dev-utilitiesboth bundle^0.2.x— if the root dep is^0.1.x(the Nx default), two separateWeakMapinstances coexist and NestJS DI metadata breaks at runtime withUnknownDependenciesException: Nest can't resolve dependencies of ConfigService. Fix: set"reflect-metadata": "^0.2.2"inpackage.jsonand re-runyarn install.
What each package does:
| Package | Purpose | Meteor equivalent |
|---|---|---|
@nestjs/graphql + @nestjs/apollo | GraphQL schema + Apollo server integration | DDP transport layer |
@as-integrations/express5 | Apollo Server v5 → Express 5 adapter (required) | (no equivalent) |
graphql | Core GraphQL library (must pin @16 on Node 20) | (no equivalent — Meteor used DDP not GraphQL) |
@nestjs/typeorm + typeorm | ORM for database operations | mongo driver integration |
pg | PostgreSQL driver | (no equivalent — Meteor used MongoDB) |
@nestjs/cqrs | Command/Query bus infrastructure | Meteor Methods mechanism |
nestjs-typed-cqrs | Type-safe return types on CQRS bus | (no equivalent — Meteor was untyped) |
nestjs-dev-utilities | AbstractEntity, AbstractDto base classes | (convention enforcer) |
@ptc-org/nestjs-query-* | Auto-generated GraphQL filtering, sorting, pagination | Minimongo’s query capabilities |
@nestjs/config | Environment variable management | Meteor.settings |
@nestjs/passport + passport-jwt | JWT authentication strategy | accounts-base |
@nestjs/jwt | JWT sign/verify | (handled by accounts package in Meteor) |
class-validator | Decorator-based validation | check() from meteor/check |
class-transformer | Transform plain objects to class instances | (no direct equivalent) |
bcrypt | Password hashing | accounts-password’s internal hashing |
5. Docker Infrastructure
Instead of Meteor’s embedded MongoDB, you run your own services in Docker containers.
5.1 Create Docker Volumes (once only)
Open Docker Desktop.
docker volume create db_volume
docker volume create redis_volume
Volumes are persistent storage on your machine. Without them, restarting a container wipes all data.
5.2 Create the Docker Compose File
Create docker-compose.dev.yml in the workspace root:
version: "3.8"
services:
postgres:
image: postgres:15-alpine
container_name: entodo_postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: entodo
ports:
- "5432:5432"
volumes:
- db_volume:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:alpine
container_name: entodo_redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_volume:/data
networks:
- app-network
adminer:
image: adminer
container_name: entodo_adminer
restart: unless-stopped
ports:
- "8080:8080"
networks:
- app-network
volumes:
db_volume:
external: true
redis_volume:
external: true
networks:
app-network:
driver: bridge
What each container does:
| Container | What it runs | Port | Meteor equivalent |
|---|---|---|---|
postgres | PostgreSQL database | 5432 | Meteor’s embedded MongoDB |
redis | Redis cache + queue broker | 6379 | (no equivalent in basic Meteor) |
adminer | Web UI to inspect the database | 8080 | Mongo Compass equivalent |
5.3 Add a Convenience Script
In package.json at the workspace root, add:
{
"scripts": {
"docker:dev": "docker compose -f docker-compose.dev.yml up -d",
"docker:stop": "docker compose -f docker-compose.dev.yml down"
}
}
Start the containers:
yarn docker:dev # Intel Mac / Linux
yarn docker:dev:arm # Apple Silicon (M1/M2/M3)
Apple Silicon: The standard
postgres:15-alpineimage runs via Rosetta 2 emulation on M-series Macs. Create a separatedocker-compose.dev.arm.ymlusing--platform linux/arm64images if you experience slowness or crashes.
Wait ~10 seconds, then verify all three are running:
docker ps
# Should show: entodo_postgres, entodo_redis, entodo_adminer
5.4 Verify Adminer
Open http://localhost:8080 in your browser and log in:
| Field | Value |
|---|---|
| System | PostgreSQL |
| Server | postgres (the container name — Docker’s internal DNS) |
| Username | postgres |
| Password | postgres |
| Database | entodo |
You should see an empty database. This is where your tables will appear after running migrations (Part 04).
Adminer is your Mongo Compass. Every table, row, and relationship in your PostgreSQL database is visible here. You will use it constantly during development to verify that migrations ran correctly and data was saved as expected.
6. Environment Variables
Create .env at the workspace root:
# ── App ─────────────────────────────────────────────────────
NODE_ENV=development
PROJECT_PORT=3333
PROJECT_GRAPHQL_PLAYGROUND=true
PROJECT_GRAPHQL_SUBSCRIPTIONS=false
# ── Database ─────────────────────────────────────────────────
PROJECT_DB_CONNECTION=postgres
PROJECT_DB_HOST=localhost
PROJECT_DB_PORT=5432
PROJECT_DB_USERNAME=postgres
PROJECT_DB_PASSWORD=postgres
PROJECT_DB_DATABASE=entodo
PROJECT_DB_DEBUG=false
# ── Database (Test) ───────────────────────────────────────────
PROJECT_DB_DATABASE_TEST=entodo_test
# ── Redis ─────────────────────────────────────────────────────
REDIS_BULL_HOST=localhost
REDIS_BULL_PORT=6379
# ── JWT (RS256 — sample keys for development only) ────────────
# In production: generate real keys and store in AWS Secrets Manager
JWT_EXPIRATION_TIME=1d
JWT_REFRESH_EXPIRATION_TIME=7d
# Paste your RSA keys here — see Part 07 for key generation
JWT_PRIVATE_KEY=
JWT_PUBLIC_KEY=
JWT_REFRESH_PRIVATE_KEY=
JWT_REFRESH_PUBLIC_KEY=
Add .env to your .gitignore (it should already be there from Nx):
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
If
.envwas ever committed before this: Adding it to.gitignoreafter the fact does not remove it from history — Git silently continues tracking changes. Run the full purge:# Strip from Git's index cache without deleting the local file git rm --cached .env git rm --cached .env.local # Commit the tracking removal immediately git commit -m "chore(git): purge local environment tracking cache"
Meteor analogy: In Meteor you used
Meteor.settings(loaded fromsettings.json) for server config. In the enterprise stack, environment variables are the standard — they work across Docker, ECS, Kubernetes, and local development without code changes.
Security rule: Never commit
.envto Git. Production values (especially JWT private keys and DB passwords) must go into AWS Secrets Manager or Tencent SSM — never in.envfiles that get deployed.
7. Configure the NestJS App Module
Replace the default apps/api/src/app/app.module.ts:
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { CqrsModule } from "@nestjs/cqrs";
import { AppResolver } from "./app.resolver";
@Module({
imports: [
// Load .env into process.env — available everywhere via ConfigService
ConfigModule.forRoot({
isGlobal: true, // no need to import ConfigModule in every feature module
envFilePath: ".env",
}),
// TypeORM: connect to PostgreSQL
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: "postgres",
host: config.get("PROJECT_DB_HOST"),
port: config.get<number>("PROJECT_DB_PORT"),
username: config.get("PROJECT_DB_USERNAME"),
password: config.get("PROJECT_DB_PASSWORD"),
database: config.get("PROJECT_DB_DATABASE"),
entities: [],
// ⚠️ Never use a glob pattern here (e.g. "**/*.entity{.ts,.js}").
// This project builds with Webpack — at runtime everything is bundled into
// main.js, so glob discovery finds nothing. Every entity must be explicitly
// imported and listed here. See Part 04 when you add the first entity.
synchronize: false, // NEVER true in production — use migrations
logging: config.get("PROJECT_DB_DEBUG") === "true",
}),
}),
// GraphQL: code-first schema generation via Apollo
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
autoSchemaFile: true, // generate schema.gql automatically from decorators
playground: config.get("PROJECT_GRAPHQL_PLAYGROUND") === "true",
context: ({ req }) => ({ req }), // pass request context (needed for guards)
}),
}),
// CQRS: registers CommandBus, QueryBus, EventBus globally
CqrsModule.forRoot(),
],
providers: [AppResolver],
})
export class AppModule {}
Add the apps/api/src/app/app.resolver.ts:
import { Query, Resolver } from "@nestjs/graphql";
@Resolver()
export class AppResolver {
@Query(() => String)
health(): string {
return "ok";
}
}
What each module does:
| Module | Purpose |
|---|---|
ConfigModule | Reads .env into process.env, provides ConfigService for typed access |
TypeOrmModule.forRootAsync | Connects to PostgreSQL, registers all entities, manages connection pool |
GraphQLModule | Starts Apollo Server, auto-generates GraphQL schema from your decorators |
CqrsModule.forRoot() | Makes CommandBus, QueryBus, and EventBus available for injection everywhere |
Policy handbook:
ConfigModuleis the hospital policy handbook. Before any wing opens for business, the handbook is read in full and every required policy entry is confirmed present. Instead of each file hardcoding database URLs or API keys, every value lives in one.envfile and any service requests it by name viaConfigService. The critical part: if a required entry is missing, the application refuses to start — fail loudly at startup, not silently at 2am in production when the missing value is first accessed.
Memory hook: ConfigModule = policy handbook. Use configService.getOrThrow('KEY') not get('KEY') — throw at startup, not at runtime.
7.1 Update main.ts
Replace apps/api/src/main.ts:
import { NestFactory } from "@nestjs/core";
import { ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import helmet from "helmet";
import { AppModule } from "./app/app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService);
// Helmet — security headers. CSP disabled in dev so Apollo Sandbox loads.
app.use(
helmet({
contentSecurityPolicy:
config.get("NODE_ENV") === "production" ? undefined : false,
})
);
// Global validation pipe — runs class-validator on every input automatically
// forbidNonWhitelisted: reject unknown fields (prevents mass-assignment attacks)
// whitelist: strip unknown fields before they reach handlers
// transform: convert plain JSON objects to typed DTO class instances
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
})
);
// CORS — allow all origins in dev, restrict in production
app.enableCors({
origin:
config.get("NODE_ENV") === "development"
? "*"
: process.env.ALLOWED_ORIGINS,
});
const port = config.get<number>("PROJECT_PORT") ?? 3333;
await app.listen(port);
console.log(`🚀 API running at http://localhost:${port}`);
console.log(`📊 GraphQL Playground: http://localhost:${port}/graphql`);
}
bootstrap();
Meteor analogy: In Meteor there was no explicit bootstrap — the framework handled startup.
main.tsis where you explicitly configure every global behaviour of your server before it starts accepting requests.
Customs hall:
ValidationPipeis the customs hall. Every incoming request must declare its exact contents before crossing the API boundary.whitelist: trueconfiscates undeclared fields.forbidNonWhitelisted: trueturns back the entire request if unknown fields appear.transform: trueconverts"42"strings to real numbers. Without this, a malicious client can injecttenantId,isAdmin, orcreatedAtinto any request body and your handlers will silently accept them.
Memory hook: ValidationPipe = customs hall. Three flags: whitelist strips, forbidNonWhitelisted rejects, transform converts. Global in main.ts = enforced on every endpoint automatically.
8. Add Nx Scripts
In package.json at the workspace root:
{
"scripts": {
"api:dev": "nx serve api",
"api:build": "nx build api",
"api:test": "nx test api",
"api:e2e": "nx e2e api-e2e",
"web:dev": "nx dev web",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"docker:dev": "docker compose -f docker-compose.dev.yml up -d",
"docker:dev:arm": "docker compose -f docker-compose.dev.arm.yml up -d",
"docker:stop": "docker compose -f docker-compose.dev.yml down",
"lint": "nx run-many --target=lint --all",
"lint:fix": "nx run-many --target=lint --all --fix",
"dep": "nx graph"
}
}
@nx/nextusesdev, notserve: The Next.js Nx plugin registersdevTargetName: "dev"(notserve). Runningnx serve webfails. Always useyarn web:devornx dev webfor the frontend.
9. Boot and Verify
Make sure Docker containers are running:
yarn docker:dev
docker ps # should show postgres, redis, adminer
Start the NestJS dev server:
yarn api:dev
You should see:
🚀 API running at http://localhost:3333
📊 GraphQL Playground: http://localhost:3333/graphql
Open http://localhost:3333/graphql. You will see the GraphQL Playground — an interactive query editor. It exposes a single health query from AppResolver (a placeholder so the schema is valid). Real queries and mutations will be added in later parts.
Meteor analogy: This is your
meteorcommand equivalent — but now you know exactly what is running and why. PostgreSQL is handling data. Redis is ready for queues. The NestJS app is serving GraphQL. The Next.js frontend will be added in Part 06.
10. Nx Workspace Commands Reference
# View the project dependency graph in browser
yarn dep
# or: npx nx graph
# Run the API dev server
yarn api:dev
# Run tests for API
yarn api:test
# Run lint on all projects
yarn lint
# See all available targets for the API app
npx nx show project api
# Check what projects are affected by uncommitted changes
npx nx affected:graph
# Clear Nx cache (if builds behave strangely)
npx nx reset
11. Git Workflow: VS Code + Commitizen
Enterprise codebases enforce Conventional Commits — a strict format that powers automated changelogs, semantic versioning, and project management integrations. yarn cz (Commitizen) enforces this format via an interactive terminal wizard.
VS Code’s built-in Source Control is the right tool for visual staging. When you stage a file in VS Code, it writes directly to the real Git index — so terminal tools like yarn cz see the staged files immediately. Standalone GUIs (like GitHub Desktop) hold their checkmarks in internal memory and only run git add a split second before their own commit button fires. This means yarn cz in the terminal will always see an empty staging area if you staged via a GUI.
VS Code Source Control Shortcuts
| Action | Mac | Windows/Linux |
|---|---|---|
| Open Source Control tab | Cmd+Shift+G | Ctrl+Shift+G |
| Toggle integrated terminal | `Cmd+“ | `Ctrl+“ |
| Stage specific lines only | Highlight in diff → right-click → Stage Selected Ranges | same |
Stage Selected Ranges is the most powerful one. If you edited a file but only want to commit part of it, highlight specific lines in the diff editor and stage only those. This makes it easy to keep commits focused without having to split your changes across multiple files manually.
Pro tip — combine Explorer + Source Control in a single pane: Click and hold the “SOURCE CONTROL” header at the top of the Source Control panel, drag it onto the Explorer icon in the activity bar, and drop it. Both views merge into one — your file tree and staged changes side by side without switching tabs.
The Commit Loop
sequenceDiagram
autonumber
actor Engineer
participant SC as VS Code Source Control
participant Term as Integrated Terminal
participant GH as Remote GitHub
Engineer->>SC: Create isolation branch (feat/feature-name)
Engineer->>SC: Implement logic, review visual diff
Engineer->>SC: Stage specific files or line ranges via '+' icon
Note over SC: Staged changes write to the real Git index immediately
Engineer->>Term: yarn lint --fix
Engineer->>Term: yarn cz
Note over Term: Interactive wizard → Conventional Commit generated
Engineer->>SC: Push branch and open Pull Request
SC->>GH: Sync local index to remote
Install Commitizen
yarn add --dev commitizen cz-conventional-changelog
Add to package.json:
{
"scripts": {
"cz": "cz"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}
Navigating the yarn cz Prompt
When you run yarn cz, the interactive terminal wizard takes over:
- Type — arrow keys to select intent:
feat(new functionality),fix(bug correction),chore(build/config updates). - Scope — the domain affected:
api,web, orworkspace. - Short description — concise, lowercase, imperative. No period at the end. Example:
setup structural base for graphql resolution. - Breaking change? — defaults to No. Yes flags a major version increment.
- Issue reference — link to your task tracker. Example:
Closes #6102.
Part 19 covers the full professional workflow: Husky pre-commit hooks, commit-msg validation, branch strategy, GitHub branch protection rules, and the complete CI/CD pipeline to production. → Git Commit Standards & CI/CD Pipeline
12. Advanced Integrations: Automated Project Management via MCP
Conventional Commits are not just cosmetic — they power automated workflows through a Model Context Protocol (MCP) Server hooked to project management layers like ClickUp and Lark.
By accurately mapping issue handles in the Commitizen footer (e.g. Closes #CLK-9421), a chain of automated updates fires the moment a Pull Request merges:
- ClickUp status updates — the target ticket moves from
In ProgresstoQA Validationwithout any manual browser interaction. - Lark broadcast alerts — an automated webhook posts a summary into the engineering channel, notifying QA leads the feature is ready for testing.
- Clean commit history — enforcing a Squash Merge Strategy on Pull Requests collapses noisy intermediate commits into one descriptive entry, keeping the log aligned with project management records.
13. Daily Engineering Checklist
Before every commit, verify:
- Did I run
git checkout -b feat/your-featurebefore modifying files? - Is
reflect-metadatapinned to^0.2.2inpackage.json? - Did I review diffs visually in VS Code Source Control — no debug logs, no unexpected files,
.envexcluded? - Did I run
yarn lint --fixbefore committing? - Did I use
yarn czto format the commit message and include the task tracker handle? - Is my Pull Request documented with a summary, ticket links, and screenshots of a successful local run?
Quick Reference
Key concepts from this part — one row per concept.
| Concept | Analogy | Meteor equivalent | The one rule |
|---|---|---|---|
| Nx monorepo | City district with zoning laws | Single meteor create directory | apps/ = separate buildings · libs/ = public post office · no direct cross-app imports |
libs/contracts | Public post office | imports/ (isomorphic) — but explicit | Only exports types; never server code on the client |
@nx/enforce-module-boundaries | City Inspector | (no equivalent) | Fires at IDE level before CI; direct app import = zoning violation |
| Docker containers | Isolated service rooms | Meteor’s embedded MongoDB | Each service starts, stops, and resets without touching your OS |
.env + ConfigModule | Policy handbook | Meteor.settings | App refuses to start if a required variable is missing |
ValidationPipe | Customs hall | check(input, String) — but optional | whitelist + forbidNonWhitelisted + transform — global, automatic |
main.ts | Ribbon-cutting ceremony | Meteor auto-bootstrap | Configures every global behaviour before the first request |
CqrsModule.forRoot() | Postal infrastructure installed | Meteor.methods mechanism | Registers CommandBus/QueryBus/EventBus globally — done once |
Summary
You have set up:
- Node 20 (via nvm), Yarn 1.x, Docker Desktop, VS Code with all extensions
- An Nx monorepo with
apps/api(NestJS),apps/web(Next.js), andlibs/contracts(shared types) - PostgreSQL + Redis + Adminer running in Docker
.envwith all required variablesAppModulewith ConfigModule, TypeORM, GraphQL, and CqrsModule wired upmain.tswith globalValidationPipeconfigured- The API running at
http://localhost:3333/graphql
| Meteor | What you just built |
|---|---|
meteor create | Nx workspace with three separate projects |
| Embedded MongoDB | PostgreSQL in Docker |
meteor (single process) | NestJS at :3333 (will add Next.js at :3000) |
| No configuration | .env + ConfigModule + ValidationPipe |