Documentation

WestJS Documentation

west-js-app is a CLI that scaffolds production-ready Express 5 + TypeScript APIs in seconds. Choose your ORM, database provider, package manager, and Docker support — the generator writes every config file, middleware, route, test, and Git hook for you.

Overview

#

west-js-app removes the tedium of bootstrapping a Node.js backend. A single command launches an interactive wizard (or accepts flags for CI/scripts) and produces a fully-wired Express 5 project — typed, linted, tested, and optionally containerised.

  • Express 5 with production middleware (helmet, cors, rate-limit, pino logging)
  • TypeScript strict mode ES2023 target, bundler module resolution
  • Four database paths: Prisma 7, Drizzle ORM, TypeORM, or no database
  • PostgreSQL, MySQL, and SQLite support for all three ORMs
  • Zod-powered environment variable validation at startup
  • Jest + ts-jest test suite with health and route integration tests
  • ESLint (flat config) + Prettier + Husky pre-commit hooks wired up
  • Docker Compose with database service auto-configured per provider
  • Vercel deployment config generated out of the box
  • Interactive prompt mode or fully non-interactive --yes flag for scripting

Requirements

#

west-js-app has minimal prerequisites. Make sure the following are available before running the CLI.

PrerequisiteVersionNotes
Node.js>= 18.0.0Required — CLI uses native fs, child_process
npm / pnpm / yarnany currentOnly the chosen PM needs to be installed
GitanyOptional — needed only if you choose to initialise a repo

Installation & Quick Start

#

Run west-js-app directly with npx — no global install required. The interactive wizard guides you through every option.

1Run the CLI: npx west-js-app
2Enter a project name (kebab-case, e.g. my-api)
3Enter an optional description and author name
4Pick a database ORM: Prisma, Drizzle, TypeORM, or None
5If a database was chosen, pick a provider: PostgreSQL, MySQL, or SQLite
6Decide whether to add Docker support (defaults to true when a DB is selected)
7Pick a package manager: npm, pnpm, or yarn
8Choose whether to initialise a Git repository
9Choose whether to install dependencies immediately
10The CLI writes every file, optionally runs git init and your package manager install, then prints the next steps
Note

Press Ctrl+C at any prompt to cancel cleanly. The CLI removes the partially-created directory on failure.

CLI Flags Reference

#

Every interactive prompt has a corresponding flag. Pass --yes (-y) to skip all prompts and accept defaults — useful for CI pipelines, scripts, and reproducible scaffolding.

FlagTypeDefaultDescription
--name <name>stringmy-appProject directory name — must be valid kebab-case
--database <orm>enumnoneORM to scaffold: prisma | drizzle | typeorm | none
--db-provider <db>enumpostgresqlDatabase provider: postgresql | mysql | sqlite
--dockerbooleanfalse (true when DB set)Generate Dockerfile + docker-compose.yml
--no-dockerbooleanExplicitly disable Docker even when a database is selected
--package-manager <pm>enumnpmPackage manager: npm | pnpm | yarn
-y, --yesbooleanfalseAccept all defaults — enables fully non-interactive mode
-v, --versionbooleanPrint the CLI version and exit
-h, --helpbooleanPrint usage information and exit

Non-interactive examples

shell

# Minimal — all defaults, name only

$
npx west-js-app --yes --name my-api

# Prisma + PostgreSQL + Docker using pnpm

$
npx west-js-app --yes --name my-api --database prisma --db-provider postgresql --docker --package-manager pnpm

# Drizzle + SQLite, no Docker

$
npx west-js-app --yes --name my-api --database drizzle --db-provider sqlite --no-docker

# TypeORM + MySQL

$
npx west-js-app --yes --name my-api --database typeorm --db-provider mysql --docker

Generated Project Structure

#

Every project gets the same core layout. Extra directories (prisma/, drizzle/, src/db/, src/generated/) are added only when a database is chosen. Docker files appear only when --docker is enabled.

my-api/
├── src/
│   ├── app.ts               # Express app factory — middleware stack assembled here
│   ├── server.ts            # HTTP server entrypoint, graceful shutdown
│   ├── routes/
│   │   ├── health.ts        # GET /health — liveness probe
│   │   └── example.ts       # CRUD example route (in-memory or DB-backed)
│   ├── middleware/
│   │   ├── error-handler.ts # Centralised error → JSON response mapper
│   │   ├── validate.ts      # Zod schema validation middleware factory
│   │   └── rate-limit.ts    # express-rate-limit configured from env
│   ├── config/
│   │   └── env.ts           # Zod-parsed, type-safe env variables
│   ├── lib/
│   │   ├── errors.ts        # AppError + HttpError base classes
│   │   └── logger.ts        # pino logger instance (pretty in dev)
│   └── db/                  # [DB only] ORM connection & schema
│       ├── index.ts
│       └── schema.ts        # [Drizzle / TypeORM]
├── tests/
│   ├── app.test.ts          # Supertest health route integration test
│   └── example.test.ts      # [no-DB only] Example route integration test
├── prisma/                  # [Prisma only]
│   └── schema.prisma
├── prisma.config.ts         # [Prisma 7 only]
├── drizzle.config.ts        # [Drizzle only]
├── .husky/
│   └── pre-commit           # Runs lint-staged on every commit
├── Dockerfile               # [Docker only] Multi-stage Node build
├── docker-compose.yml       # [Docker only] App + DB services
├── .dockerignore            # [Docker only]
├── vercel.json              # Vercel serverless deployment config
├── jest.config.js           # ts-jest preset
├── eslint.config.mjs        # ESLint flat config (typescript-eslint)
├── .prettierrc              # Prettier rules
├── tsconfig.json            # TypeScript — strict, ES2023, bundler resolution
├── package.json             # Scripts, dependencies, lint-staged config
├── .env                     # Local environment variables (git-ignored)
└── .env.example             # Committed env template
Note

The .env file is always git-ignored. The .env.example is committed so team members know which variables to set.

Core Generated Files

#

Each file has a specific responsibility. Understanding them helps you extend the project confidently.

src/app.ts — Express Application Factory

Creates and exports the Express app. Applies JSON body parsing, URL-encoded parsing, CORS (via cors), security headers (via helmet), HTTP request logging (via pino-http), rate limiting, and mounts all routers. The app is exported separately from the server so it can be imported in tests without binding a port.

  • helmet() sets secure HTTP headers (XSS, clickjacking, MIME sniffing protection)
  • cors() is configured with permissive defaults tighten in production
  • pino-http logs every request/response with duration and status code
  • express.json() + express.urlencoded() parse incoming request bodies
  • rateLimiter middleware is applied globally before routes
  • Routes are mounted under their respective prefixes (/health, /api/examples)

src/server.ts — HTTP Server Entrypoint

Imports the app, binds it to the port from env, and registers SIGTERM/SIGINT handlers for graceful shutdown. When TypeORM is selected, the server waits for initializeDatabase() to resolve before binding the port — preventing requests before the DB connection is ready.

  • Reads PORT from the validated env config (defaults to 3000)
  • Logs the running port and NODE_ENV with pino
  • SIGTERM / SIGINT close the HTTP server then destroy the DB connection (TypeORM)
  • TypeORM variant calls initializeDatabase() before app.listen()

src/config/env.ts — Environment Variable Validation

Uses Zod to parse process.env at startup. If a required variable is missing or the wrong type, the process exits immediately with a readable error — preventing silent misconfiguration in production.

// Shape of the parsed env object
{
  NODE_ENV: 'development' | 'production' | 'test',
  PORT: number,              // coerced from string
  RATE_LIMIT_WINDOW_MS: number,
  RATE_LIMIT_MAX: number,
  DATABASE_URL: string       // only present when a DB is selected
}

src/lib/errors.ts — Error Classes

Exports AppError (base class with statusCode and isOperational flag) and HTTP-specific subclasses like NotFoundError (404) and ValidationError (422). Throwing these anywhere in the app causes the error handler middleware to produce a consistent JSON response.

  • AppError extends Error carry statusCode and message
  • Operational errors (AppError subclasses) return JSON responses
  • Non-operational errors (programming bugs) log full stack and return 500
  • isOperational flag lets you distinguish expected vs unexpected failures

src/lib/logger.ts — Pino Logger

Creates a named pino logger instance. In development (NODE_ENV !== production) pino-pretty is used for human-readable, coloured log output. In production, plain JSON logs are emitted for log aggregation tools.

  • Log level auto-set to 'debug' in development, 'info' in production
  • pino-pretty provides coloured, formatted output during local development
  • Structured JSON logs in production work with Datadog, Loki, CloudWatch etc.

src/middleware/error-handler.ts — Error Handler

Express error-handling middleware (4-argument signature). Catches all errors thrown or passed to next(). Operational AppError instances are returned as JSON with their statusCode. Everything else returns 500 and logs the full error details server-side.

// Response shape for all errors
{ "error": { "message": "Not found", "status": 404 } }

src/middleware/validate.ts — Zod Validation Middleware

A middleware factory: validate(schema) returns an Express RequestHandler that parses req.body against the given Zod schema. On failure it calls next() with a ValidationError (422). On success it replaces req.body with the parsed (type-safe) value.

// Usage in a route
router.post('/', validate(CreateItemSchema), createItem);

src/middleware/rate-limit.ts — Rate Limiter

Configures express-rate-limit using RATE_LIMIT_WINDOW_MS and RATE_LIMIT_MAX from the validated env. Exceeding the limit returns a 429 Too Many Requests response in standard JSON format.

  • Window: 900000 ms (15 minutes) by default change in .env
  • Max requests per window: 100 by default
  • Response after limit: { error: 'Too many requests' }

Generated npm Scripts

#

All scripts are written into the generated project's package.json. Database scripts (db:*) are only added when an ORM is selected.

ScriptCommandPurpose
devtsx watch src/server.tsHot-reload dev server — no build step needed
buildtscCompile TypeScript to dist/ using tsconfig.json
startnode dist/server.jsRun the compiled production build
testNODE_ENV=test jestRun the test suite with Jest
linteslint src/Lint all files in src/ using the flat ESLint config
lint:fixeslint src/ --fixAuto-fix all fixable lint errors
formatprettier --write "src/**/*.ts"Format all TypeScript source files
format:checkprettier --check "src/**/*.ts"Check formatting without writing
preparehusky || trueInstall Husky hooks after npm install (silently skipped if Husky unavailable)
db:generateprisma generate / drizzle-kit generateGenerate the ORM client / migration files
db:migrateprisma migrate dev / drizzle-kit migrateRun pending database migrations
db:pushprisma db push / drizzle-kit pushPush schema changes without a migration file
db:studioprisma studio / drizzle-kit studioOpen the visual database browser

Default API Endpoints

#

The scaffold ships with a health route and a fully-wired CRUD example route. Remove or rename the example route before shipping to production.

MethodPathHandlerDescription
GET/inlineWelcome message with project name and current version
GET/healthhealthRouteHandlerReturns { status: 'ok', timestamp } — suitable for load balancer probes
GET/api/examplesgetExamplesReturns array of all example items
GET/api/examples/:idgetExampleByIdReturns a single item by ID or 404
POST/api/examplescreateExampleValidates body with Zod schema, creates item, returns 201
DELETE/api/examples/:iddeleteExampleDeletes item by ID or returns 404
Note

When a database ORM is selected, the example route uses the real DB. Without a database it uses an in-memory array — useful for getting started without any infrastructure.

Database Support

#

west-js-app supports three ORMs across three database providers. When you pick an ORM, the CLI writes the config file, connection module, an example schema or entity, and wires up the db:* scripts.

ORMPostgreSQLMySQLSQLiteConfig fileSchema/Entity file
Prisma 7prisma/schema.prisma + prisma.config.tsprisma/schema.prisma
Drizzle ORMdrizzle.config.tssrc/db/schema.ts
TypeORMsrc/db/data-source.tssrc/db/entities/Example.ts
None

Prisma 7 Setup

#

west-js-app targets Prisma 7, which introduced a required driver adapter pattern. Unlike Prisma 4/5, you must explicitly pass a driver adapter to the PrismaClient constructor — the classic environment-variable-only approach no longer works.

Note

The generated client lives in src/generated/prisma — do not edit those files. Regenerate them whenever you change schema.prisma.

What the CLI generates

  • prisma/schema.prisma generator block with output pointing to src/generated/prisma
  • prisma.config.ts Prisma 7 defineConfig() with dotenv + datasource URL
  • src/lib/prisma.ts PrismaClient instantiated with the correct driver adapter
  • src/db/index.ts re-exports db for use across the project
  • src/db/schema.ts example User model to get started

Driver adapters per provider

ProviderAdapter packageDriver packageAdapter class
PostgreSQL@prisma/adapter-pgpg + @types/pgPrismaPg
MySQL@prisma/adapter-mysqlmysql2PrismaMysql
SQLite@prisma/adapter-better-sqlite3better-sqlite3 + @types/better-sqlite3PrismaBetterSqlite3

First-run workflow

1Update DATABASE_URL in .env with your connection string
2Run: npm run db:generate (generates the Prisma client into src/generated/prisma)
3Run: npx prisma migrate dev --name init (creates the first migration and applies it)
4Run: npm run dev (starts the dev server)

Drizzle ORM Setup

#

Drizzle is a lightweight, SQL-first TypeScript ORM. The CLI generates a drizzle.config.ts, per-provider connection setup in src/db/index.ts, and an example schema in src/db/schema.ts.

What the CLI generates

  • drizzle.config.ts dialect, schema path, output path, and DB credentials
  • src/db/index.ts provider-specific driver initialisation (postgres, mysql2, better-sqlite3)
  • src/db/schema.ts example 'examples' table definition using Drizzle column helpers

Drivers used per provider

ProviderDriver packageDrizzle import
PostgreSQLpostgres (postgres.js)drizzle-orm/postgres-js
MySQLmysql2drizzle-orm/mysql2
SQLitebetter-sqlite3 + @types/better-sqlite3drizzle-orm/better-sqlite3

First-run workflow

1Update DATABASE_URL in .env
2Run: npm run db:push (pushes schema directly — good for early dev)
3Or run: npm run db:generate && npm run db:migrate (formal migration flow)
4Run: npm run dev

TypeORM Setup

#

TypeORM uses class decorators to define entities. The CLI enables experimentalDecorators and emitDecoratorMetadata in tsconfig.json and generates a DataSource configuration, an Example entity, and database initialisation logic.

What the CLI generates

  • src/db/data-source.ts TypeORM DataSource with provider-appropriate driver and entity path
  • src/db/entities/Example.ts @Entity decorated class with @PrimaryGeneratedColumn, @Column
  • src/db/index.ts initializeDatabase() function that initialises the DataSource
  • tsconfig.json experimentalDecorators: true, emitDecoratorMetadata: true added automatically

reflect-metadata

TypeORM requires reflect-metadata. The CLI adds it to dependencies and imports it at the top of src/app.ts — import 'reflect-metadata' must appear before any TypeORM imports.

Note

Forgetting to import reflect-metadata is the most common TypeORM error. The generator handles this for you.

First-run workflow

1Update DATABASE_URL in .env
2Run: npm run db:generate (generates a migration from your entities)
3Run: npm run db:migrate (runs pending migrations)
4Run: npm run dev

Docker Support

#

When Docker is enabled, the CLI generates three files: a multi-stage Dockerfile, a docker-compose.yml with both the app service and a database service, and a .dockerignore. The Docker database service is pre-configured for the chosen provider.

Note

Run docker compose up --build to start both services. The app service waits for the database to be healthy before starting.

Dockerfile

Multi-stage build: a 'builder' stage installs dependencies and compiles TypeScript, then a lean 'production' stage copies only the compiled output and production node_modules. This keeps the final image small.

# Stage 1 — build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 — production image
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]

docker-compose.yml

Defines two services: your Express app (built from the Dockerfile) and the database. The DB service image and port are auto-selected based on your provider choice.

ProviderDocker imagePort
PostgreSQLpostgres:16-alpine5432
MySQLmysql:8-debian3306
SQLitefile-based, no DB service needed

Environment Variables

#

The CLI writes .env (git-ignored) and .env.example (committed) with sensible defaults. All variables are validated by Zod at startup in src/config/env.ts.

# ── Runtime ──────────────────────────────────────────
NODE_ENV=development
PORT=3000

# ── Rate limiting ────────────────────────────────────
RATE_LIMIT_WINDOW_MS=900000   # 15 minutes
RATE_LIMIT_MAX=100             # max requests per window per IP

# ── Database (only when an ORM is selected) ──────────
# PostgreSQL (Prisma)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

# PostgreSQL (Drizzle / TypeORM)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

# MySQL
DATABASE_URL="mysql://user:password@localhost:3306/mydb"

# SQLite
DATABASE_URL="file:./dev.db"   # Prisma
DATABASE_URL="./dev.db"        # Drizzle / TypeORM
Note

Never commit .env. The .env.example file is checked in so team members know which variables to configure.

TypeScript Configuration

#

The generated tsconfig.json is optimised for modern Node.js development. It uses strict mode to catch type errors early and bundler moduleResolution for compatibility with ESM packages.

OptionValueWhy
targetES2023Use modern JS features (structuredClone, Array.at, etc.)
moduleESNextEmit ESM output
moduleResolutionbundlerResolves package.json exports — compatible with tsx and bundlers
stricttrueEnables all strict type checks (null checks, implicit any, etc.)
outDir./distCompiled output goes to dist/
rootDir./srcSource root — only src/ is compiled
esModuleInteroptrueAllows default imports from CommonJS modules
skipLibChecktrueSkip type-checking of .d.ts files in node_modules
sourceMaptrueSource maps for debugging compiled JS
resolveJsonModuletrueAllow import of .json files
experimentalDecoratorstrue (TypeORM only)Required for TypeORM @Entity / @Column decorators
emitDecoratorMetadatatrue (TypeORM only)Required for TypeORM reflection metadata

ESLint + Prettier

#

The generated project ships with ESLint flat config (eslint.config.mjs) and a .prettierrc. Husky + lint-staged run both tools on every git commit so your codebase stays clean automatically.

eslint.config.mjs

Uses typescript-eslint's tseslint.config() helper to compose the eslint recommended + typescript-eslint recommended rule sets. Unused variables prefixed with _ are ignored to allow intentional placeholders.

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    },
  },
  { ignores: ['dist/', 'node_modules/', 'coverage/', 'jest.config.js'] }
);

Husky pre-commit hook

lint-staged is configured in package.json and runs on every commit. It lints and formats only the staged .ts files — keeping commits fast by avoiding a full project scan.

// package.json — lint-staged config
"lint-staged": {
  "*.ts": ["eslint --fix", "prettier --write"]
}

Testing

#

Projects are scaffolded with Jest + ts-jest + Supertest. Two integration tests are generated: an app health-check test that works universally, and an example route test that is only generated when no database is selected (the in-memory variant makes it predictable).

Note

Run npm test — Jest sets NODE_ENV=test automatically via the script. The test server binds to a random port to avoid port conflicts when running in parallel.

jest.config.js

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/tests/**/*.test.ts'],
  moduleNameMapper: {
    // map .js extensions back to .ts source files for ts-jest
    '^(\\.{1,2}/.+)\\.js$': '$1',
  },
};

Generated test files

FileTestsAlways generated
tests/app.test.tsGET /health → 200, body.status === 'ok'Yes
tests/example.test.tsGET /api/examples, POST, GET /:id, DELETE /:idOnly when database = none

Vercel Deployment

#

A vercel.json is always generated. It configures a serverless Node build that routes every request to dist/server.js, sets a 60-second function timeout, 1024 MB memory, and permissive CORS headers.

{
  "version": 2,
  "builds": [
    {
      "src": "dist/server.js",
      "use": "@vercel/node",
      "config": { "maxDuration": 60, "memory": 1024 }
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "dist/server.js",
      "methods": ["GET","POST","PUT","DELETE","PATCH","OPTIONS"],
      "headers": {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "*"
      }
    }
  ]
}
Note

Remember to add your environment variables in the Vercel project settings. DATABASE_URL in particular must be set for any DB-backed project.

Contributing

#

west-js-app is an open-source monorepo. The CLI lives in packages/cli, the documentation website in web/. All contributions are welcome.

1Fork and clone the repository
2Run: pnpm install (from repo root — installs all workspace packages)
3Run: pnpm test (runs the CLI test suite)
4Make your changes in packages/cli/src/
5Run: pnpm -F west-js-app run build (to verify compilation)
6Test the built CLI: node packages/cli/dist/index.js --yes --name test-output
7Commit using Conventional Commits format — Husky enforces commitlint
8Open a pull request against the main branch

Useful development commands

shell

# Clone

$
git clone https://github.com/vishalvoid/west.js.git && cd west.js

# Install all workspace dependencies

$
pnpm install

# Run CLI tests

$
pnpm -F west-js-app test

# Build CLI

$
pnpm -F west-js-app build

# Test the built output locally

$
node packages/cli/dist/index.js --help
$
node packages/cli/dist/index.js --yes --name my-test-app --database drizzle --db-provider sqlite

# Run web dev server

$
pnpm dev