| name | nodejs-project-setup |
| description | Guides expert-level Node.js project initialization: ESM vs CJS module system decision, package.json configuration, Node.js version management, engine locking, and production deployment configuration.
Use when the user asks about Node.js project setup, ESM vs CommonJS, package.json configuration, Node.js version management, engine field, exports field.
Do NOT use when the user asks about TypeScript setup (use `typescript-project-setup`), JavaScript idioms (use `javascript-idioms`), Node.js async patterns (use `nodejs-async-patterns`).
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"javascript backend template","category":"software-engineering","subcategory":"languages-runtimes","depends":"","disclaimer":"none","difficulty":"intermediate"} |
Node.js Project Setup
When to Use
Use this skill when the user:
- Is initializing a new Node.js project from scratch and needs guidance on the full configuration stack
- Is deciding between ESM (ES Modules) and CommonJS and needs a concrete recommendation based on their context
- Needs to configure the
package.json exports field, main, module, or type fields correctly
- Wants to lock Node.js versions via
.nvmrc, .node-version, or the engines field in package.json
- Is setting up a Node.js library for npm publication and needs proper entry point configuration
- Is configuring a Node.js application for production deployment (Docker, serverless, PaaS) and needs engine locking, start scripts, and health check patterns
- Asks about
packageManager field, Corepack, or enforcing a specific package manager version on a team
Do NOT use this skill when the user:
- Needs TypeScript configuration,
tsconfig.json tuning, or declaration file generation -- use typescript-project-setup
- Asks about JavaScript language patterns like closures, prototypes, or async/await -- use
javascript-idioms
- Needs help with async concurrency patterns, event loop understanding, or Promise chains -- use
nodejs-async-patterns
- Asks about monorepo workspace tooling in depth (Turborepo, Nx, Lerna) -- use a dedicated monorepo skill
- Needs a full CI/CD pipeline design beyond basic npm scripts -- use a CI/CD configuration skill
- Is asking about Deno or Bun as a runtime -- those have meaningfully different module and config systems
Process
1. Gather Project Context Before Generating Any Configuration
Establish these facts before writing a single line of configuration. Wrong decisions here cascade into painful migrations later.
- Project type: Is this a library (published to npm), an application (deployed to a server or container), or a CLI tool? Libraries require dual-format output consideration and a precise
exports map. Applications only need to satisfy their own runtime.
- Deployment target: Cloud Run, AWS Lambda, Fly.io, a raw VPS, or a Docker container each have different Node.js version availability, cold-start constraints, and filesystem assumptions. Lambda's managed runtime pins you to specific LTS versions.
- Team size and experience: Solo projects can use bleeding-edge tooling. A team of 5+ needs enforced consistency via Corepack,
engines pinning, and pre-commit hooks.
- Minimum Node.js version requirement: If integrating with existing infrastructure, you may be constrained. If greenfield, target the current LTS (Node.js 20 as of 2024, Node.js 22 entering LTS in late 2024).
- Is there an existing codebase? If yes, determine its
type field setting and whether changing it (from CJS to ESM) is feasible without a flag day migration.
2. Make the ESM vs CommonJS Decision Explicitly
This is the most consequential choice and the one most often made by accident. Apply this decision framework:
-
Choose ESM ("type": "module" in package.json) when:
- Starting a new project with no legacy constraints
- All key dependencies support ESM (check with
node --input-type=module or inspect their exports map)
- The project is a library and you want to ship a single ESM-only package (acceptable since Node.js 12.17+)
- Using top-level
await, named exports, or dynamic import() heavily
- Targeting modern runtimes only (Node.js 18+)
-
Choose CommonJS (no "type" field, or "type": "commonjs") when:
- Integrating with a large body of existing CJS code that uses
require() extensively
- Key dependencies are CJS-only and do not have an ESM export condition (check their
package.json exports field for the "import" condition)
- Deploying to environments where the Node.js version is below 12.17
- The project is a library consumed by a mixed CJS/ESM ecosystem and you cannot provide a dual build
-
Choose a dual-format library build (CJS + ESM output) when:
- Publishing a library to npm that must support consumers on both module systems
- Using a build tool like
tsup, esbuild, or rollup to emit both .mjs/.cjs artifacts
- Setting up separate
exports conditions for "import" and "require"
- NOTE: Do NOT attempt to maintain dual format by hand -- always use a build tool
-
Key interop rules to communicate:
- ESM can import CJS via
import (Node.js will wrap the CJS module), but CJS cannot require() an ESM module -- this is a hard error
.mjs extension forces ESM; .cjs extension forces CJS, regardless of the "type" field
__dirname and __filename do not exist in ESM -- use import.meta.url with new URL('.', import.meta.url).pathname or the helper from
3. Configure package.json Fields in Correct Order
Build the package.json field by field with precise semantics. Do not omit fields that affect resolution or publication.
name: Use scoped names (@scope/package) for organizational packages or to avoid naming conflicts. All lowercase, hyphen-separated. Max 214 characters.
version: Use semver. Start at 0.1.0 for unreleased packages, 1.0.0 when you commit to a stable API. Use 0.x.y range while the API is volatile.
type: Set explicitly to "module" or "commonjs". Never rely on the default (CJS) as a silent assumption.
main: The CJS fallback entry point. Required for Node.js versions below 12. If ESM-only, still set it to point to your ESM entry for older tooling that ignores exports.
exports: The modern entry point map. Takes precedence over main in Node.js 12+. This is required for libraries. See the Output Format section for the exact structure.
files: Whitelist what ships in the npm tarball. Always include your dist/ or src/ directory. Always exclude node_modules, test files, and local config. The .npmignore approach is error-prone -- prefer files.
engines: Declare the minimum Node.js and npm/pnpm/yarn versions. This is advisory by default but can be made mandatory with engine-strict=true in .npmrc.
packageManager: Use this field with Corepack to enforce the exact package manager and version. Example: "packageManager": "pnpm@9.1.0". Run corepack enable once per machine to activate enforcement.
scripts: Keep script names consistent across projects. Use start for production server, dev for development with watch mode, build for compilation, test for test runner, lint for linter, format for formatter.
dependencies vs devDependencies: Production code dependencies go in . Build tools, test runners, linters, and type checkers go in . For libraries, be conservative with -- every entry becomes a peer dependency concern for consumers.
4. Set Up Node.js Version Management
Version drift between local development and production is a common source of subtle bugs. Layer multiple enforcement mechanisms.
.nvmrc file: A single line containing the Node.js version string (e.g., 20.14.0 or lts/iron). Used by nvm, fnm, and mise. Place at the project root. Commit it to source control.
.node-version file: Same format as .nvmrc. Recognized by fnm, volta, and mise. Some teams maintain both; others pick one. Prefer .nvmrc for broadest tooling compatibility.
engines field in package.json: Declare "node": ">=20.0.0" (or a tighter range like ">=20.14.0 <21.0.0" for applications). This causes npm/pnpm/yarn to warn (or error with strict mode) when the installed version does not match.
.npmrc with engine-strict=true: Upgrades the engines field from a warning to a hard error during npm install. For team projects, add engine-strict=true to the project-level .npmrc and commit it.
- Volta (
volta field in package.json): Volta pins Node.js and package manager versions at the per-project level and auto-switches on directory entry without manual nvm use. Add: "volta": { "node": "20.14.0", "pnpm": "9.1.0" }.
- Docker base image pinning: In production containers, pin the exact image digest or tag:
FROM node:20.14.0-alpine3.20. Never use node:latest or node:lts in production Dockerfiles -- these are mutable tags.
- CI version matrix: In GitHub Actions or similar, test against the minimum declared engine version AND the current LTS using a matrix strategy. This catches regressions from version-specific behavior.
5. Configure the exports Map for Libraries
The exports field is the single most important and most misunderstood field for library authors. Get this right.
- Condition order matters: Node.js evaluates conditions in the order they appear in the object. Place
"types" first (for TypeScript consumers), then "import", then "require", then "default".
- Subpath exports: Use
"." for the main export and named subpaths like "./utils" for secondary entry points. This replaces deep require paths like require('mylib/dist/utils').
- Export blocking: Any path NOT listed in
exports is blocked from direct access in Node.js 12+ strict mode. Use this intentionally to prevent consumers from importing internal modules.
- Wildcard patterns: Use
"./features/*": "./dist/features/*.js" to expose an entire directory while still enforcing the .js extension.
- The
package.json self-referencing export: Always include "./package.json": "./package.json" so consumers can read your package metadata.
- Dual-package hazard: When shipping both CJS and ESM, there is a risk of the package being instantiated twice (once as CJS, once as ESM) in the same process, breaking singleton patterns. Mitigate with a wrapper-only CJS build that re-exports the ESM implementation, or use the
exports map carefully to prevent dual loading.
6. Configure Development Tooling and Scripts
Set up the minimum viable toolchain for a productive Node.js development experience.
- Watch mode (applications): Use
node --watch src/index.js (available since Node.js 18.11.0) for zero-dependency file watching. For more complex reload logic, nodemon remains the practical standard: nodemon --ext js,json,mjs src/index.js.
- Environment variables: Use
node --env-file=.env (available since Node.js 20.6.0) to load .env files natively without dotenv. For Node.js below 20.6, dotenv is the standard. Always add .env to .gitignore. Commit a .env.example with all keys but no values.
- Linting: ESLint with
@eslint/js and eslint-plugin-n (the Node.js plugin). Configure with the flat config format (eslint.config.js) for Node.js 18+ projects. Enable plugin:n/recommended to catch Node.js-specific mistakes like using unavailable APIs.
- Formatting: Prettier for zero-config opinionated formatting. The key Prettier setting for Node.js projects:
"trailingComma": "all" (aids git diffs), "singleQuote": true (common JS convention), "printWidth": 100.
- Pre-commit hooks:
simple-git-hooks (lightweight, 0 dependencies) or husky (more features). Run eslint and prettier --check on staged files via lint-staged. This prevents bad code from entering the repository.
- Testing: Use the built-in Node.js test runner (
node:test module, stable since Node.js 20) for new projects to eliminate dependencies. For more features (coverage, snapshot testing, mocking), vitest is the modern choice for ESM-native testing.
7. Configure Production Readiness
Production configuration is distinct from development setup and must be explicit.
NODE_ENV=production: Set this environment variable in production to enable production optimizations in Express, Next.js, and many other frameworks. Many packages short-circuit development checks, heap profiling, and detailed error messages when this is set.
- Process manager: For bare VPS or container deployments, use
pm2 in cluster mode for multi-core utilization: pm2 start src/index.js -i max --name app. For containerized deployments (Docker, Kubernetes), run Node.js directly as PID 1 with a SIGTERM handler -- do not wrap in a process manager inside containers.
- Graceful shutdown: Register
process.on('SIGTERM', ...) and process.on('SIGINT', ...) handlers. Close HTTP servers with server.close(), drain database connection pools, and flush any buffers before calling process.exit(0). Give the process 10-30 seconds maximum.
--max-old-space-size: Set heap limits explicitly in production. A Node.js process defaults to ~1.5GB on 64-bit systems. In containers, set this to 75-80% of the container's memory limit: node --max-old-space-size=3072 src/index.js for a 4GB container.
- Structured logging: Replace
console.log with a structured logger. pino is the standard for high-throughput production Node.js -- it serializes JSON at near-zero overhead. Never use winston for new projects (it is slower and more complex without benefit).
- Health check endpoint: Every deployed application must expose
GET /health returning 200 OK with response body {"status": "ok", "uptime": process.uptime()}. Orchestrators (Kubernetes, ECS) use this for liveness probes.
Output Format
When generating a Node.js project setup, produce ALL of the following artifacts in order.
Project Structure
my-app/
├── .nvmrc # Node.js version pin (e.g., "20.14.0")
├── .npmrc # npm/pnpm configuration (engine-strict=true)
├── .env.example # Environment variable template
├── .gitignore # Node.js-specific ignore patterns
├── eslint.config.js # ESLint flat config
├── package.json # Full configuration (see below)
├── src/
│ └── index.js # Application entry point
└── test/
└── index.test.js # Initial smoke test
package.json -- Application Template
{
"name": "my-app",
"version": "0.1.0",
"description": "Short description of the application",
"type": "module",
"engines": {
"node": ">=20.14.0",
"pnpm": ">=9.0.0"
},
"packageManager": "pnpm@9.1.0",
"volta": {
"node": "20.14.0",
"pnpm": "9.1.0"
},
"scripts": {
"start": "node src/index.js",
"dev": "node --watch --env-file=.env src/index.js",
"test"
package.json -- Library Template (Dual-Format Build)
{
"name": "@scope/my-lib",
"version": "1.0.0",
"description": "Short description of the library",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./utils"
.npmrc
engine-strict=true
save-exact=true
.nvmrc
20.14.0
.gitignore
# Dependencies
node_modules/
# Build output
dist/
build/
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage
coverage/
.nyc_output/
# Editor directories
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
# OS
.DS_Store
Thumbs.db
eslint.config.js (Flat Config, ESLint 9+)
import js from '@eslint/js'
import pluginN from 'eslint-plugin-n'
export default [
js.configs.recommended,
pluginN.configs['flat/recommended'],
{
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'n/no-missing-import': 'error',
'n/no-unpublished-import': 'error',
'n/prefer-global/buffer': ['error', 'always'],
'n/prefer-global/process': ['error', 'always'],
},
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
},
},
]
src/index.js -- Application Entry Point Shell
import { createServer } from 'node:http'
const PORT = process.env.PORT ?? 3000
const HOST = process.env.HOST ?? '0.0.0.0'
const server = createServer((req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }))
return
}
res.writeHead(404)
res.end()
})
server.listen(PORT, HOST, () => {
console.log(`Server listening on http://${HOST}:${PORT}`)
})
const shutdown = (signal) => {
console.warn()
server.( {
.()
process.()
})
( {
.()
process.()
}, ).()
}
process.(, ())
process.(, ())
Verification Checklist
| Check | Command | Expected Result |
|---|
| Node.js version | node --version | Matches .nvmrc |
| Package manager | pnpm --version | Matches packageManager field |
| Install succeeds | pnpm install | No engine warnings or errors |
| Lint passes | pnpm lint | Zero violations |
| Format check | pnpm format:check | All files formatted |
| Tests pass | pnpm test | All tests green |
| Server starts | pnpm start | Prints listen message |
| Health endpoint | curl localhost:3000/health | {"status":"ok","uptime":...} |
Rules
-
Never omit the "type" field. Its absence defaults to "commonjs", which is an invisible assumption that breaks when someone later adds an .mjs file or a dependency that is ESM-only. Always declare "type": "module" or "type": "commonjs" explicitly so the intent is documented.
-
Never use bare node: builtins without the node: prefix in new ESM code. Always write import { readFile } from 'node:fs/promises' instead of import { readFile } from 'fs/promises'. The node: prefix prevents ambiguity with npm packages of the same name, is the documented modern practice, and is required for certain built-ins in some runtimes (Deno, Bun) for compatibility.
-
Never use "exports" subpath patterns that expose internal implementation paths. Once you list a path in exports, it becomes a public API surface. Unlisted paths are blocked. Use this as a feature -- intentionally exclude src/internal/** to prevent consumers from depending on internals.
-
Always run npm pack --dry-run (or pnpm pack --dry-run) before publishing a library to verify that the files field is correct and the tarball contains exactly what you intend. Shipping node_modules, test fixtures, or .env files in a published package is a security and size issue.
-
Never use * (wildcard) version ranges for dependencies in production applications. Use save-exact=true in .npmrc or pin versions manually. Floating ranges (^, ~) are acceptable in libraries (to allow consumers flexibility) but not in applications where reproducibility is critical.
-
Never mix CJS require() and ESM import syntax in the same file. In a "type": "module" project, require is not defined at the top level. In a CJS project, top-level await and static import are syntax errors. If you need to bridge module systems, use dynamic import() (works in CJS) or a conditional build.
Edge Cases
ESM-Only Dependency in a CJS Project
When a key dependency drops CJS support (this happened with chalk v5, node-fetch v3, nanoid v4, and many others), a CJS project cannot require() it. Options in order of preference:
- Pin to the last CJS-compatible version (e.g.,
chalk@4, node-fetch@2) if migration is not feasible right now.
- Migrate the entire project to ESM -- often the cleanest long-term solution.
- Use a dynamic
import() to load the ESM module from within CJS code. This forces the call site to be async and propagates await up the call chain.
- Use an alternative package that still ships CJS (
kleur instead of chalk, native fetch instead of node-fetch on Node.js 18+).
Do NOT advise using createRequire to load ESM -- it does not work and will produce a clear error.
Migrating an Existing CJS Codebase to ESM
This is a multi-step process that cannot be done atomically in large codebases. Guide the user through this sequence:
- Audit all
require() calls to identify dynamic requires (require(someVariable)) -- these have no ESM equivalent and must be replaced with dynamic import().
- Audit all uses of
__dirname and __filename -- replace with the fileURLToPath(new URL('.', import.meta.url)) pattern.
- Add file extensions to all relative imports. ESM requires explicit extensions (
./utils.js, not ./utils). This is the most tedious step in large codebases.
- Change
module.exports = ... to export default ... or named exports.
- Change all
require() calls to import statements.
- Set
"type": "module" in package.json.
- Run tests. Address any remaining interop issues.
Never attempt steps 4-7 before 1-3 are complete. Use a codemod tool like @es-migrations/named-exports or a custom AST transform with jscodeshift for large codebases -- manual edits across hundreds of files will introduce inconsistencies.
Lambda and Serverless Constraints
AWS Lambda's managed Node.js runtimes lag behind the current LTS. As of 2024, Lambda supports Node.js 18.x and 20.x as managed runtimes. Key constraints:
- Pin
"engines" to exactly the Lambda runtime version ("node": "=20.9.0" for Lambda's 20.x runtime).
- ESM is supported on Lambda with the
"type": "module" approach, but cold start times are measurably higher for ESM bundles due to module graph resolution. For latency-sensitive functions, benchmark both and consider bundling with esbuild into a single CJS file to eliminate this overhead.
- The
--env-file flag is not useful on Lambda -- use process.env directly, populated by Lambda environment variable configuration.
SIGTERM is sent to Lambda functions 300ms before the execution context freezes -- your shutdown handler has 300ms, not 30 seconds. Size your shutdown logic accordingly.
Monorepo with Mixed Module Systems
When a monorepo contains packages that are CJS and packages that are ESM, isolation is critical:
- Each package must have its own
package.json with an explicit "type" field. The root package.json's "type" field does NOT apply to subpackages.
- Use workspace-level ESLint configuration with per-package overrides for module system rules.
- In pnpm workspaces, use
pnpm --filter <package-name> <command> to run scripts in a specific package to avoid cross-package contamination.
- Cross-package imports within the monorepo should always go through the package's published
exports map, never via relative paths that bypass the exports field. This is enforced by setting "moduleResolution": "bundler" or "node16" in TypeScript, or via eslint-plugin-n rules.
Corepack and packageManager Field Enforcement
The packageManager field works only when Corepack is enabled. The workflow for new contributors:
- Document in
README.md that contributors must run corepack enable once after installing Node.js.
- If a contributor uses the wrong package manager (e.g., runs
npm install in a pnpm-managed workspace), Corepack will block the command with a clear error message.
- Corepack downloads the specified package manager version on demand -- contributors do not need to install pnpm separately.
- For CI, add
- run: corepack enable as the first setup step before any install commands.
- Pin the
packageManager field to a full semver string including patch version ("pnpm@9.1.0", not "pnpm@9"). Corepack uses this to download the exact binary.
Node.js Version Mismatch Between Team Members
When team members are on different Node.js versions and experiencing inconsistent behavior:
- Confirm
engine-strict=true is in the project .npmrc. If not, add it -- this turns engine mismatch warnings into install-blocking errors.
- Add a
.nvmrc check to the preinstall npm lifecycle script: "preinstall": "node -e \"const v=process.version; const r='>=20.14.0'; require('semver').satisfies(v,r)||process.exit(1)\"". This requires semver as a devDependency.
- In GitHub Actions, pin the Node.js version with
node-version-file: '.nvmrc' in the actions/setup-node step. This automatically reads the .nvmrc file rather than requiring manual updates to the workflow YAML.
- Document the version requirement in
README.md with exact instructions for both nvm and fnm users.
Example
Input: "I'm building a new REST API with Node.js that will run on Fly.io. The team is 3 developers. I want to use ESM, pnpm, and make sure everyone is on the same Node.js version. What should I set up?"
Analysis:
- Project type: Application (not a library -- no need for
exports map or dual format)
- Deployment target: Fly.io -- a container-based platform, so Dockerfile and graceful shutdown matter
- Team size: 3 -- Corepack enforcement and pre-commit hooks are worth the setup cost
- Module system: ESM explicitly requested
- Package manager: pnpm explicitly requested
- Node.js version: Use current LTS (20.14.0) since Fly.io runs whatever you put in the Dockerfile
Output:
.nvmrc
20.14.0
.npmrc
engine-strict=true
save-exact=true
package.json
{
"name": "my-api",
"version": "0.1.0",
"description": "REST API deployed to Fly.io",
"private": true,
"type": "module",
"engines": {
"node": ">=20.14.0",
"pnpm": ">=9.0.0"
},
"packageManager": "pnpm@9.1.0",
"volta": {
"node": "20.14.0",
"pnpm": "9.1.0"
},
"scripts": {
"start": "node --max-old-space-size=512 src/index.js",
"dev"
Key decisions made:
"private": true -- this is an application, not an npm package
--max-old-space-size=512 -- Fly.io's free tier VMs have 256-512MB RAM; 512MB heap leaves headroom for OS overhead
pino as the logger -- production-grade, JSON output, compatible with Fly.io's log aggregation
save-exact=true -- application should have reproducible installs; no floating ranges
src/index.js
import { createServer } from 'node:http'
import { createLogger } from './logger.js'
const log = createLogger()
const PORT = process.env.PORT ?? 8080
const HOST = '0.0.0.0'
const server = createServer((req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }))
return
}
log.info({ method: req.method, url: req.url }, 'Request received')
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ : }))
})
server.(, , {
log.({ : }, )
})
= () => {
log.({ signal }, )
server.( {
log.()
process.()
})
( {
log.()
process.()
}, ).()
}
process.(, ())
process.(, ())
process.(, {
log.({ reason }, )
process.()
})
process.(, {
log.({ err }, )
process.()
})
src/logger.js
import pino from 'pino'
export const createLogger = () => pino({
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
})
eslint.config.js
import js from '@eslint/js'
import pluginN from 'eslint-plugin-n'
export default [
js.configs.recommended,
pluginN.configs['flat/recommended'],
{
rules: {
'no-console': 'error',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'n/no-missing-import': 'error',
'n/no-process-env': 'warn',
'n/prefer-global/process': ['error', 'always'],
},
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
},
settings: {
n: { version: '>=20.14.0' },
},
},
]
.env.example
NODE_ENV=development
PORT=8080
LOG_LEVEL=info
test/index.test.js
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
describe('Application smoke test', () => {
it('creates a logger without throwing', async () => {
const { createLogger } = await import('../src/logger.js')
const log = createLogger()
assert.ok(log, 'Logger should be created')
assert.equal(typeof log.info, 'function', 'Logger should have info method')
})
})
README.md section -- Team Setup
## Development Setup
**Prerequisites:**
- Node.js 20.14.0 (use nvm: `nvm use`, or fnm: `fnm use`)
- Enable Corepack (once per machine): `corepack enable`
**First-time setup:**
\`\`\`bash
corepack enable # Activates pnpm via Corepack
nvm use # Switches to Node.js 20.14.0
pnpm install # Install dependencies (engine version enforced)
cp .env.example .env # Create local environment file
pnpm dev # Start development server with watch mode
\`\`\`
Verification Run
node --version
pnpm --version
pnpm install
pnpm lint
pnpm format:check
pnpm test
pnpm start &
curl http://localhost:8080/health