DEV Community

Alex Chen
Alex Chen

Posted on

The Node.js Setup I Use on Every New Project (2026 Edition)

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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,
          })),
        },
      });
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)