Skip to content
KheAi
Go back

Environment Setup & Nx Workspace

Edit page

What This Part Covers

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:

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-boundaries is 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? The code command isn’t in your PATH yet. Install it from VS Code:

  1. Open VS Code
  2. Press Cmd+Shift+P and run: Shell Command: Install 'code' command in PATH
  3. Restart your terminal

Then retry:

code --install-extension nrwl.angular-console

Alternatively, 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:


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:

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/). The apps/ directory is where apps/api (your NestJS server) and apps/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):

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.ts is your server/main.js. app.module.ts is the root of everything the server knows about — equivalent to all your server/ imports combined.

Ribbon-cutting: main.ts is the ribbon-cutting ceremony. Before any wing (module) can open for business, the ceremony unlocks the doors (NestFactory.create), registers global pipes, validates the .env policy 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:

@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.css not globals.css: Nx generates the file as global.css (no trailing s). The import in layout.tsx already points to the correct name — don’t rename it.

Tailwind v4 vs v3: V4 removes tailwind.config.js and the three @tailwind directives. Source file detection is automatic. Custom theme extensions use @theme blocks 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.tsx references 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 to display: 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 your client/ directory. But now it is a completely separate application — it communicates with apps/api only through HTTP, not through shared memory.

3.4 Generate the Shared Contracts Library

npx nx g @nx/js:lib libs/contracts --bundler=tsc

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/contracts is 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/api and apps/web are separate buildings — each with its own zoning permit. libs/contracts is 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-metadata gotcha: After installing, check that reflect-metadata is ^0.2.2 in your root package.json. typeorm and nestjs-dev-utilities both bundle ^0.2.x — if the root dep is ^0.1.x (the Nx default), two separate WeakMap instances coexist and NestJS DI metadata breaks at runtime with UnknownDependenciesException: Nest can't resolve dependencies of ConfigService. Fix: set "reflect-metadata": "^0.2.2" in package.json and re-run yarn install.

What each package does:

PackagePurposeMeteor equivalent
@nestjs/graphql + @nestjs/apolloGraphQL schema + Apollo server integrationDDP transport layer
@as-integrations/express5Apollo Server v5 → Express 5 adapter (required)(no equivalent)
graphqlCore GraphQL library (must pin @16 on Node 20)(no equivalent — Meteor used DDP not GraphQL)
@nestjs/typeorm + typeormORM for database operationsmongo driver integration
pgPostgreSQL driver(no equivalent — Meteor used MongoDB)
@nestjs/cqrsCommand/Query bus infrastructureMeteor Methods mechanism
nestjs-typed-cqrsType-safe return types on CQRS bus(no equivalent — Meteor was untyped)
nestjs-dev-utilitiesAbstractEntity, AbstractDto base classes(convention enforcer)
@ptc-org/nestjs-query-*Auto-generated GraphQL filtering, sorting, paginationMinimongo’s query capabilities
@nestjs/configEnvironment variable managementMeteor.settings
@nestjs/passport + passport-jwtJWT authentication strategyaccounts-base
@nestjs/jwtJWT sign/verify(handled by accounts package in Meteor)
class-validatorDecorator-based validationcheck() from meteor/check
class-transformerTransform plain objects to class instances(no direct equivalent)
bcryptPassword hashingaccounts-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:

ContainerWhat it runsPortMeteor equivalent
postgresPostgreSQL database5432Meteor’s embedded MongoDB
redisRedis cache + queue broker6379(no equivalent in basic Meteor)
adminerWeb UI to inspect the database8080Mongo 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-alpine image runs via Rosetta 2 emulation on M-series Macs. Create a separate docker-compose.dev.arm.yml using --platform linux/arm64 images 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:

FieldValue
SystemPostgreSQL
Serverpostgres (the container name — Docker’s internal DNS)
Usernamepostgres
Passwordpostgres
Databaseentodo

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 .env was ever committed before this: Adding it to .gitignore after 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 from settings.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 .env to Git. Production values (especially JWT private keys and DB passwords) must go into AWS Secrets Manager or Tencent SSM — never in .env files 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:

ModulePurpose
ConfigModuleReads .env into process.env, provides ConfigService for typed access
TypeOrmModule.forRootAsyncConnects to PostgreSQL, registers all entities, manages connection pool
GraphQLModuleStarts Apollo Server, auto-generates GraphQL schema from your decorators
CqrsModule.forRoot()Makes CommandBus, QueryBus, and EventBus available for injection everywhere

Policy handbook: ConfigModule is 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 .env file and any service requests it by name via ConfigService. 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.ts is where you explicitly configure every global behaviour of your server before it starts accepting requests.

Customs hall: ValidationPipe is the customs hall. Every incoming request must declare its exact contents before crossing the API boundary. whitelist: true confiscates undeclared fields. forbidNonWhitelisted: true turns back the entire request if unknown fields appear. transform: true converts "42" strings to real numbers. Without this, a malicious client can inject tenantId, isAdmin, or createdAt into 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/next uses dev, not serve: The Next.js Nx plugin registers devTargetName: "dev" (not serve). Running nx serve web fails. Always use yarn web:dev or nx dev web for 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 meteor command 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

ActionMacWindows/Linux
Open Source Control tabCmd+Shift+GCtrl+Shift+G
Toggle integrated terminal`Cmd+“`Ctrl+“
Stage specific lines onlyHighlight in diff → right-click → Stage Selected Rangessame

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"
    }
  }
}

When you run yarn cz, the interactive terminal wizard takes over:

  1. Type — arrow keys to select intent: feat (new functionality), fix (bug correction), chore (build/config updates).
  2. Scope — the domain affected: api, web, or workspace.
  3. Short description — concise, lowercase, imperative. No period at the end. Example: setup structural base for graphql resolution.
  4. Breaking change? — defaults to No. Yes flags a major version increment.
  5. 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:


13. Daily Engineering Checklist

Before every commit, verify:

  1. Did I run git checkout -b feat/your-feature before modifying files?
  2. Is reflect-metadata pinned to ^0.2.2 in package.json?
  3. Did I review diffs visually in VS Code Source Control — no debug logs, no unexpected files, .env excluded?
  4. Did I run yarn lint --fix before committing?
  5. Did I use yarn cz to format the commit message and include the task tracker handle?
  6. 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.

ConceptAnalogyMeteor equivalentThe one rule
Nx monorepoCity district with zoning lawsSingle meteor create directoryapps/ = separate buildings · libs/ = public post office · no direct cross-app imports
libs/contractsPublic post officeimports/ (isomorphic) — but explicitOnly exports types; never server code on the client
@nx/enforce-module-boundariesCity Inspector(no equivalent)Fires at IDE level before CI; direct app import = zoning violation
Docker containersIsolated service roomsMeteor’s embedded MongoDBEach service starts, stops, and resets without touching your OS
.env + ConfigModulePolicy handbookMeteor.settingsApp refuses to start if a required variable is missing
ValidationPipeCustoms hallcheck(input, String) — but optionalwhitelist + forbidNonWhitelisted + transform — global, automatic
main.tsRibbon-cutting ceremonyMeteor auto-bootstrapConfigures every global behaviour before the first request
CqrsModule.forRoot()Postal infrastructure installedMeteor.methods mechanismRegisters CommandBus/QueryBus/EventBus globally — done once

Summary

You have set up:

MeteorWhat you just built
meteor createNx workspace with three separate projects
Embedded MongoDBPostgreSQL in Docker
meteor (single process)NestJS at :3333 (will add Next.js at :3000)
No configuration.env + ConfigModule + ValidationPipe

Edit page
Share this post:

Next Post
TypeScript Decorators, NestJS DI & the Module System
Previous Post
From Meteor Magic to NestJS Enterprise Clarity