The Node.js Setup I Use on Every New Project (2026 Edition)
This is my complete starter setup. Copy it, customize it, start building.
Step 1: Initialize
mkdir my-project && cd my-project
npm init -y
# Essential packages
npm install express zod helmet cors morgan dotenv
npm install -D typescript @types/node @types/express tsup jest @types/jest nodemon tsx eslint prettier
# Git init
git init
echo "node_modules/\ndist/\n.env\n.env.local" > .gitignore
Step 2: TypeScript Config
// tsconfig.json — Strict but practical
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noUncheckedIndexedAccess": true, // Catches arr[0] undefined bugs
"exactOptionalPropertyTypes": true, // { x?: string } ≠ { x: undefined }
"noImplicitReturns": true, // Missing return catches
"noFallthroughCasesInSwitch": true // Missing break catches
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Step 3: Project Structure
my-project/
├── src/
│ ├── index.ts # Entry point
│ ├── app.ts # Express app setup
│ ├── routes/
│ │ ├── health.ts # Health check
│ │ └── api/
│ │ └── v1/
│ │ └── users.ts # User routes
│ ├── middleware/
│ │ ├── auth.ts # Auth middleware
│ │ ├── errorHandler.ts # Global error handler
│ │ └── validate.ts # Request validation
│ ├── services/
│ │ └── user.service.ts # Business logic
│ ├── types/
│ │ └── index.ts # Shared types
│ └── utils/
│ ├── logger.ts # Structured logging
│ └── response.ts # Standardized responses
├── tests/
│ └── health.test.ts
├── .env.example
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
Step 4: The Entry Point
// src/index.ts — Minimal, clean entry point
import 'dotenv/config'; // Load .env before anything else
import app from './app.js';
import { logger } from './utils/logger.js';
const PORT = parseInt(process.env.PORT || '3000', 10);
const server = app.listen(PORT, () => {
logger.info(`Server running on :${PORT} (${process.env.NODE_ENV})`);
});
// Graceful shutdown
function shutdown(signal: string) {
logger.info(`${signal} received. Shutting down gracefully...`);
server.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
// Force exit after 10s
setTimeout(() => process.exit(1), 10_000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Step 5: Express App
// src/app.ts — All middleware and routes
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { errorHandler } from './middleware/errorHandler.js';
import { requestLogger } from './middleware/requestLogger.js';
import healthRoutes from './routes/health.js';
import userRoutes from './routes/api/v1/users.js';
const app = express();
// Trust proxy (behind Nginx/reverse proxy)
app.set('trust proxy', 1);
// Security
app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGINS?.split(',') || false }));
// Body parsing (with limits)
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// Logging
if (process.env.NODE_ENV !== 'test') {
app.use(morgan('combined'));
}
app.use(requestLogger());
// Routes
app.use('/health', healthRoutes);
app.use('/api/v1/users', userRoutes);
// Error handler (MUST be last)
app.use(errorHandler);
export default app;
Step 6: Key Utilities
// src/utils/response.ts — Standard API response format
export function success(res: Express.Response, data: unknown, status = 200) {
return res.status(status).json({
data,
meta: { requestId: res.req.id },
});
}
export function error(res: Express.Response, message: string, status = 500, code = 'ERROR') {
return res.status(status).json({
error: { code, message },
meta: { requestId: res.req.id },
});
}
// src/utils/logger.ts — JSON logger for production
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export const logger = {
debug: (msg: string, obj?: object) => log('debug', msg, obj),
info: (msg: string, obj?: object) => log('info', msg, obj),
warn: (msg: string, obj?: object) => log('warn', msg, obj),
error: (msg: string, obj?: object) => log('error', msg, obj),
};
function log(level: LogLevel, msg: string, obj?: object) {
const entry = {
level,
time: new Date().toISOString(),
msg,
...(obj && { data: obj }),
};
if (level === 'error' || level === 'warn') {
console.error(JSON.stringify(entry));
} else {
console.log(JSON.stringify(entry));
}
}
// src/middleware/errorHandler.ts — Global error handler
import { Request, Response, NextFunction } from 'express';
export function errorHandler(err: Error, req: Request, res: Response, _next: NextFunction) {
const statusCode = (err as any).statusCode || 500;
logger.error(`[${req.method} ${req.path}] ${err.message}`, {
stack: err.stack,
requestId: req.id,
});
res.status(statusCode).json({
error: {
code: (err as any).code || 'INTERNAL_ERROR',
message: statusCode === 500 && process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
},
meta: { requestId: req.id },
});
}
// src/middleware/validate.ts — Zod validation wrapper
import { ZodSchema } from 'zod';
export function validate(schema: ZodSchema) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const body = await schema.parseAsync(req.body);
req.body = body;
next();
} catch (err: any) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: err.errors.map((e: any) => ({
field: e.path.join('.'),
issue: e.message,
})),
},
});
}
};
}
Step 7: Sample Route with Full Pattern
// src/routes/api/v1/users.ts
import { Router } from 'express';
import { validate } from '../../middleware/validate.js';
import { success, error } from '../../utils/response.js';
import { createUserSchema, updateUserSchema } from '../../types/user.js';
import * as userService from '../../services/user.service.js';
const router = Router();
// POST /api/v1/users
router.post('/', validate(createUserSchema), async (req, res, next) => {
try {
const user = await userService.create(req.body);
return success(res, user, 201);
} catch (err) {
next(err);
}
});
// GET /api/v1/users/:id
router.get('/:id', async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
if (!user) return error(res, 'User not found', 404, 'NOT_FOUND');
return success(res, user);
} catch (err) {
next(err);
}
});
// PUT /api/v1/users/:id
router.put('/:id', validate(updateUserSchema), async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
if (!user) return error(res, 'User not found', 404, 'NOT_FOUND');
return success(res, user);
} catch (err) {
next(err);
}
});
// DELETE /api/v1/users/:id
router.delete('/:id', async (req, res, next) => {
try {
const deleted = await userService.remove(req.params.id);
if (!deleted) return error(res, 'User not found', 404, 'NOT_FOUND');
return success(res, null); // 204 would be better but keeping consistent
} catch (err) {
next(err);
}
});
export default router;
Step 8: Package.json Scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsup src/index.ts --format esm --dts",
"start": "node dist/index.js",
"test": "jest --forceExit",
"test:watch": "jest --watch",
"lint": "eslint src/",
"format": "prettier --write 'src/**/*.ts'",
"typecheck": "tsc --noEmit"
}
}
Step 9: Environment Template
# .env.example — Commit this, don't commit .env!
NODE_ENV=development
PORT=3000
CORS_ORIGINS=http://localhost:5173
DATABASE_URL=postgresql://localhost:5432/mydb
JWT_SECRET=change-me-in-production
LOG_LEVEL=debug
Step 10: npm Scripts Cheatsheet
| Command | Does | When to use |
|---|---|---|
npm run dev |
Start with hot reload | Development |
npm run build |
Compile TS → JS | Before deploying |
npm run start |
Run compiled output | Production |
npm test |
Run tests once | CI/CD |
npm run test:watch |
Re-run on file change | TDD workflow |
npm run lint |
Check code style | Pre-commit |
npm run format |
Auto-fix formatting | After editing |
npm run typecheck |
Type check without build | Quick type check |
What This Gives You
✅ Type-safe request/response handling
✅ Consistent error responses across all endpoints
✅ Structured JSON logging
✅ Input validation with clear error messages
✅ Graceful shutdown handling
✅ CORS + security headers configured
✅ Rate limiting ready (add middleware)
✅ Health check endpoint for monitoring
✅ Test framework ready
✅ Hot-reload in development
From zero to production-ready in ~15 minutes of copy-paste.
What's YOUR essential Node.js setup? Anything I'm missing?
Follow @armorbreak for more Node.js content.
Top comments (0)