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) enterprise-todo backend accessible at http://localhost:3333/graphql.


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.


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 GitHub Copilot: Optimized tool selectionThe code command isn’t available in your PATH. 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 enterprise-todo --preset=apps

When prompted:

cd enterprise-todo

Your directory now looks like:

enterprise-todo/
├── 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:

npm install --save-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.

3.3 Generate the Next.js Frontend

npx nx g @nx/next:app apps/web --src=true --appDir=true --style=tailwind

When prompted:

This creates apps/web/ with a Next.js 14 App Router app pre-configured with Tailwind CSS.

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).


4. Install Backend Dependencies

From the workspace root:

yarn add @nestjs/graphql @nestjs/apollo graphql @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

What each package does:

PackagePurposeMeteor equivalent
@nestjs/graphql + @nestjs/apolloGraphQL schema + Apollo server integrationDDP transport layer
graphqlCore GraphQL library(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: enterprise_todo_postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: enterprise_todo
    ports:
      - "5432:5432"
    volumes:
      - db_volume:/var/lib/postgresql/data
    networks:
      - app-network

  redis:
    image: redis:alpine
    container_name: enterprise_todo_redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_volume:/data
    networks:
      - app-network

  adminer:
    image: adminer
    container_name: enterprise_todo_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

Wait ~10 seconds, then verify all three are running:

docker ps
# Should show: enterprise_todo_postgres, enterprise_todo_redis, enterprise_todo_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
Databaseenterprise_todo

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=enterprise_todo
PROJECT_DB_DEBUG=false

# ── Database (Test) ───────────────────────────────────────────
PROJECT_DB_DATABASE_TEST=enterprise_todo_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

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: [__dirname + "/**/*.entity{.ts,.js}"],
        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

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 { AppModule } from "./app/app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = app.get(ConfigService);

  // 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.


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",
    "docker:dev": "docker compose -f docker-compose.dev.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"
  }
}

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

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 :4200)
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