Node.js project structure skill
Gives a Node.js project a professional TypeScript layout in one run, and leaves every existing file alone.
- skill
- Tested with Claude Code 2.1.300 on an empty npm project
- Updated 2026-10-16
- node, typescript, scaffolding
What you get
- .env.exampleEvery variable env.ts reads, with a safe placeholder value.
- .prettierrcFormatting: single quotes, trailing commas.
- eslint.config.mjsBare ESLint config that only ignores dist and node_modules. Add rules when you install ESLint.
- package.jsonExisting file, kept. Only the scripts block changed: dev, build, start, test, lint.
- src/All application code.
- config/Configuration.
- env.tsReads process.env once, validates PORT, exports a typed object. The only file that reads process.env.
- index.tsEntry point. Loads config, starts an HTTP server, closes it on SIGTERM.
- lib/Small shared helpers.
- errors.tsAppError class that carries an HTTP status.
- logger.tsJSON logger with info and error methods.
- routes/One file per resource goes here. Empty because the project has no web framework.
- .gitkeepKeeps the empty folder in git.
- services/Business logic goes here, with no HTTP types.
- .gitkeepKeeps the empty folder in git.
- tests/Tests, mirroring src/.
- config/Tests for src/config.
- env.test.tsChecks that env.port is a number.
- lib/Tests for src/lib.
- errors.test.tsChecks that AppError defaults to status 500.
- tsconfig.jsonStrict TypeScript, ES2022 target, compiles src to dist.
.env.example
NODE_ENV=development PORT=3000 LOG_LEVEL=info
src/config/env.ts
const nodeEnv = process.env.NODE_ENV ?? 'development';
const port = Number(process.env.PORT ?? 3000);
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`Invalid PORT: ${process.env.PORT}`);
}
export const env = {
nodeEnv,
port,
logLevel: process.env.LOG_LEVEL ?? 'info',
} as const;src/index.ts
import { createServer } from 'node:http';
import { env } from './config/env.ts';
import { logger } from './lib/logger.ts';
const server = createServer((_req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' }).end('ok');
});
server.listen(env.port, () => logger.info('listening', { port: env.port }));
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down');
server.close(() => process.exit(0));
});tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"skipLibCheck": true
},
"include": ["src"]
}It will not touch: existing source files, node_modules, .git, .env, anything listed in .gitignore.
Commands it runs: npm test
From a real run on an empty npm project with Claude Code 2.1.300, 63 seconds.
- ---
- name: node-project-structure
- description: Give a Node.js project a professional TypeScript structure without touching existing code
- ---
- Set up a professional structure for the Node.js project in the current directory.
- Before changing anything:
- 1. Read package.json and list the existing top-level files and folders.
- 2. Never overwrite or move an existing file. If a file this skill would create already exists, leave it and say so in the summary.
- 3. Never touch node_modules, .git, .env, or any file listed in .gitignore.
- Create what is missing, in this layout:
- - src/index.ts: entry point. Loads config, starts the app, handles SIGTERM.
- - src/config/env.ts: reads process.env once, validates it, exports a typed object. No other file reads process.env.
- - src/routes/: one file per resource, each exporting a router.
- - src/services/: business logic. No HTTP types in this folder.
- - src/lib/: small shared helpers (logger, errors).
- - tests/: mirrors src/. Use the test runner already in package.json; if none, use node:test.
- - .env.example: every variable env.ts reads, with a safe placeholder value.
- - tsconfig.json: strict, target ES2022, outDir dist, rootDir src.
- - eslint.config.mjs and .prettierrc: only if the project has neither.
- Then update package.json scripts only where missing: dev, build, start, test, lint. npm's default test script (`echo "Error: no test specified" && exit 1`) counts as missing.
- Run the test script once and report the pass and fail counts. If it cannot run, say why.
- Finish with a summary in this form:
- created: <paths>
- skipped (already existed): <paths>
- next step: <one command to run>
- Do not install packages. Do not add a web framework the project does not already use.
What it does
Reads your package.json and existing folders, then creates the missing parts of a standard TypeScript layout: a typed config module, routes, services, tests, and the config files around them. It reports what it created and what it skipped.
Advertisement
When to use it
- Starting a new Node.js service and you want a sane layout before writing code.
- Cleaning up a project that grew without one, when you want the missing pieces added and nothing moved.
Install
Download the file or copy it using the actions above.
Save it as
.claude/skills/node-project-structure/SKILL.mdin your repo.Run
/node-project-structurein Claude Code from the project root.
Advertisement