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.

SKILL.md
Download
  1. ---
  2. name: node-project-structure
  3. description: Give a Node.js project a professional TypeScript structure without touching existing code
  4. ---
  5. Set up a professional structure for the Node.js project in the current directory.
  6. Before changing anything:
  7. 1. Read package.json and list the existing top-level files and folders.
  8. 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.
  9. 3. Never touch node_modules, .git, .env, or any file listed in .gitignore.
  10. Create what is missing, in this layout:
  11. - src/index.ts: entry point. Loads config, starts the app, handles SIGTERM.
  12. - src/config/env.ts: reads process.env once, validates it, exports a typed object. No other file reads process.env.
  13. - src/routes/: one file per resource, each exporting a router.
  14. - src/services/: business logic. No HTTP types in this folder.
  15. - src/lib/: small shared helpers (logger, errors).
  16. - tests/: mirrors src/. Use the test runner already in package.json; if none, use node:test.
  17. - .env.example: every variable env.ts reads, with a safe placeholder value.
  18. - tsconfig.json: strict, target ES2022, outDir dist, rootDir src.
  19. - eslint.config.mjs and .prettierrc: only if the project has neither.
  20. 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.
  21. Run the test script once and report the pass and fail counts. If it cannot run, say why.
  22. Finish with a summary in this form:
  23. created: <paths>
  24. skipped (already existed): <paths>
  25. next step: <one command to run>
  26. 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

  1. Download the file or copy it using the actions above.

  2. Save it as .claude/skills/node-project-structure/SKILL.md in your repo.

  3. Run /node-project-structure in Claude Code from the project root.

Advertisement