---
# === IDENTITY ===
id: software/migrations/php-to-nodejs/2026
canonical_question: "How do I migrate a PHP backend to Node.js?"
aliases:
  - "PHP to Node.js migration guide"
  - "convert PHP to Node.js"
  - "replace PHP with Node.js"
  - "PHP to Express migration"
  - "PHP to NestJS migration"
  - "modernize PHP backend to Node"
  - "PHP to JavaScript backend"
entity_type: software_reference
domain: software > migrations > php_to_nodejs
region: global
jurisdiction: global
temporal_scope: 2018-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.92
freshness: evolving
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Node.js 24 LTS, 2025-10-28"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Node.js is single-threaded and event-driven — never block the event loop with CPU-intensive synchronous work; use worker_threads or offload to a queue"
  - "PHP's request-per-process isolation does not exist in Node.js — module-level variables are shared across all concurrent requests"
  - "PHP sessions (server-side files) have no Node.js built-in equivalent — use express-session with Redis or switch to JWT"
  - "Express 5.x requires Node.js 18+; Node.js 24 (Krypton) is the current LTS as of Oct 2025 — target Node.js 22 or 24, not 18 (EOL Apr 2025) or 20 (in maintenance)"
  - "Hosting differs fundamentally: PHP runs on Apache/Nginx + PHP-FPM; Node.js needs a process manager (PM2, Docker, systemd) for production stability"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Migrating PHP to Python (Django/Flask/FastAPI)"
    use_instead: "software/migrations/php-to-python/2026"
  - condition: "Migrating PHP to Go for high-performance systems"
    use_instead: "software/migrations/php-to-go/2026"
  - condition: "Upgrading Express to NestJS (already on Node.js)"
    use_instead: "software/migrations/express-to-nestjs/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "php_framework"
    question: "Which PHP framework is the current codebase using?"
    type: choice
    options: ["Laravel", "Symfony", "CodeIgniter", "CakePHP", "Vanilla PHP (no framework)", "Other"]
  - key: "app_type"
    question: "What type of application is this?"
    type: choice
    options: ["REST API", "Server-rendered web app (MVC)", "Real-time app (WebSocket/SSE)", "Background job processor", "Monolithic full-stack"]
  - key: "team_experience"
    question: "How experienced is your team with JavaScript/TypeScript?"
    type: choice
    options: ["Strong (daily JS/TS)", "Moderate (some frontend JS)", "Minimal (PHP-only team)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/php-to-nodejs/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/php-to-python/2026"
      label: "PHP to Python Migration"
    - id: "software/migrations/php-to-go/2026"
      label: "PHP to Go Migration"
    - id: "software/migrations/rails-to-nodejs/2026"
      label: "Rails to Node.js Migration"
  solves:
    - id: "software/migrations/express-to-nestjs/2026"
      label: "Express to NestJS Migration"
    - id: "software/migrations/express-to-fastify/2026"
      label: "Express to Fastify Migration"
  alternative_to:
    - id: "software/migrations/php-to-python/2026"
      label: "PHP to Python Migration"
    - id: "software/migrations/php-to-go/2026"
      label: "PHP to Go Migration"
  often_confused_with:
    - id: "software/migrations/jquery-to-react/2026"
      label: "jQuery to React Migration (frontend, not backend)"

# === SOURCES (9 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Node.js Documentation — Getting Started"
    author: Node.js
    url: https://nodejs.org/en/learn/getting-started/introduction-to-nodejs
    type: official_docs
    published: 2025-10-29
    reliability: authoritative
  - id: src2
    title: "Express 5.1.0 — Now the Default on npm with LTS Timeline"
    author: Express.js
    url: https://expressjs.com/2025/03/31/v5-1-latest-release.html
    type: official_docs
    published: 2025-03-31
    reliability: authoritative
  - id: src3
    title: "PHP Migration Strategy: Node.js, Python, React (2026)"
    author: LegacyLeap
    url: https://www.legacyleap.ai/blog/php-migration/
    type: technical_blog
    published: 2026-01-15
    reliability: high
  - id: src4
    title: "From PHP to Node.js: A Backend Migration Strategy and Comparison"
    author: NamasteDev
    url: https://namastedev.com/blog/from-php-to-node-js-a-backend-migration-strategy-and-comparison/
    type: technical_blog
    published: 2025-06-10
    reliability: high
  - id: src5
    title: "Node.js vs PHP: A Head-to-Head Comparison"
    author: Kinsta
    url: https://kinsta.com/blog/node-js-vs-php/
    type: technical_blog
    published: 2025-01-20
    reliability: high
  - id: src6
    title: "Announcing NestJS 11: What's New"
    author: Trilon
    url: https://trilon.io/blog/announcing-nestjs-11-whats-new
    type: technical_blog
    published: 2025-12-10
    reliability: high
  - id: src7
    title: "Knex.js Migrations Documentation"
    author: Knex.js
    url: https://knexjs.org/guide/migrations
    type: official_docs
    published: 2025-06-01
    reliability: high
  - id: src8
    title: "Node.js 24 Becomes LTS: What You Need to Know"
    author: NodeSource
    url: https://nodesource.com/blog/nodejs-24-becomes-lts
    type: technical_blog
    published: 2025-10-28
    reliability: high
  - id: src9
    title: "NestJS v12 Roadmap: Full ESM Migration, Standard Schema Validation and Modernised Toolchain"
    author: InfoQ
    url: https://www.infoq.com/news/2026/04/nestjs-12-roadmap-esm/
    type: technical_blog
    published: 2026-04-15
    reliability: high
---

# How to Migrate a PHP Backend to Node.js

## TL;DR

- **Bottom line**: Migrate incrementally using the strangler fig pattern -- extract PHP endpoints into Node.js services one at a time behind a reverse proxy, converting synchronous PHP patterns to async Node.js equivalents with Express 5 or NestJS 11.
- **Key tool/command**: `npx express-generator --no-view myapp && npm install` or `npx @nestjs/cli new myapp`
- **Watch out for**: Treating PHP's synchronous, request-per-process model as if it maps 1:1 to Node.js -- Node.js is single-threaded and event-driven, so blocking the event loop with CPU-intensive work (like PHP's `sleep()` or tight loops) will freeze all concurrent requests.
- **Works with**: Node.js 22 (Maintenance LTS) / 24 (Active LTS, Krypton, since Oct 2025), Express 5.x, NestJS 11+ (NestJS 12 alpha released Q2 2026 — full ESM + Vitest/oxlint/Rspack), Fastify 5+, PHP 7.4-8.4 (source side). [src8, src9]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Async paradigm shift**: Node.js is single-threaded and event-loop-driven. CPU-intensive synchronous operations (image processing, PDF generation, tight loops) block ALL concurrent requests. Use `worker_threads`, child processes, or offload to a message queue. [src1]
- **No request isolation**: PHP gives each request its own process with isolated memory. Node.js shares module-level state across all concurrent requests. Never store request-scoped data in global or module-level variables -- use `req.locals` or `res.locals`. [src5]
- **Session handling redesign required**: PHP sessions use server-side files by default (`$_SESSION`). Node.js has no built-in session mechanism. You must implement `express-session` with a Redis/Memcached store, or switch to JWT for stateless auth. [src4]
- **Hosting architecture differs**: PHP runs behind Apache/Nginx + PHP-FPM, which automatically manages worker processes. Node.js requires a process manager (PM2, Docker, systemd) for zero-downtime restarts, multi-core utilization, and crash recovery. [src3]
- **Express 5.x minimum Node.js 18**: Verify runtime version before starting. Node.js 18 reached EOL April 2025, Node.js 20 entered maintenance October 2025, Node.js 22 entered maintenance. **Node.js 24 (Krypton) is the Active LTS as of October 2025** — target it for new migrations. [src2, src8]

## Quick Reference

| PHP Pattern | Node.js Equivalent | Example |
|---|---|---|
| `$_GET['param']` / `$_POST['param']` | `req.query.param` / `req.body.param` | `app.get('/users', (req, res) => { const id = req.query.id; })` |
| `PDO::query($sql)` | Knex query builder or Prisma Client | `const users = await knex('users').where('active', true)` |
| `$_SESSION['user']` | `express-session` + Redis store or JWT | `req.session.user = { id: 1, name: 'Alice' }` |
| `middleware()` via PHP frameworks | `app.use(middleware)` | `app.use(express.json())` or `app.use(cors())` |
| `move_uploaded_file()` | `multer` middleware | `upload.single('avatar')` then `req.file.path` |
| `require_once 'template.php'` | Template engine (EJS/Pug) or API-only | `res.render('index', { title: 'Home' })` or `res.json(data)` |
| `cron` + PHP script | `node-cron` or OS-level cron | `cron.schedule('0 * * * *', () => { runJob() })` |
| `try/catch` + `throw` | `try/catch` + async error middleware | `app.use((err, req, res, next) => { res.status(500).json({error: err.message}) })` |
| `password_hash()` / `password_verify()` | `bcrypt` package | `const hash = await bcrypt.hash(password, 12)` |
| `getenv('DB_HOST')` / `.env` files | `dotenv` + `process.env` | `require('dotenv').config(); const host = process.env.DB_HOST` |
| `composer require package` | `npm install package` | `npm install express cors helmet` |
| `json_encode()` / `json_decode()` | `JSON.stringify()` / `JSON.parse()` | `res.json({ status: 'ok' })` -- Express calls `JSON.stringify` internally |
| `header('Content-Type: ...')` | `res.set()` or `res.type()` | `res.type('json')` or `res.set('X-Custom', 'value')` |
| `http_response_code(404)` | `res.status(404)` | `res.status(404).json({ error: 'Not found' })` |
| `file_get_contents($url)` | `fetch()` (built-in Node 18+) | `const data = await fetch(url).then(r => r.json())` |

## Decision Tree

```
START
├── Is this a complete rewrite or incremental migration?
│   ├── COMPLETE REWRITE → New Node.js project (NestJS 11 for large teams, Express 5 for small, Fastify 5 for performance-critical)
│   └── INCREMENTAL ↓
├── Is the PHP app a monolith or already service-oriented?
│   ├── MONOLITH → Use strangler fig pattern: put Nginx/HAProxy in front, route new endpoints to Node.js
│   └── SERVICE-ORIENTED → Replace services one at a time with Node.js equivalents
├── Does the PHP app use a framework (Laravel, Symfony)?
│   ├── LARAVEL → Map concepts: routes→Express routes, Eloquent→Prisma/Knex, middleware→Express middleware, Blade→EJS/API-only
│   ├── SYMFONY → Map concepts: controllers→NestJS controllers, Doctrine→Prisma, DI→NestJS DI, EventDispatcher→NestJS interceptors
│   └── VANILLA PHP → Identify all entry points (index.php, AJAX endpoints), convert each to an Express route
├── Is the workload CPU-intensive (image processing, PDF generation)?
│   ├── YES → Keep those in PHP or use worker threads / child processes in Node.js
│   └── NO (I/O-bound: DB queries, API calls, file reads) ↓
├── Does the team know TypeScript?
│   ├── YES → Use NestJS 11 (structured, decorators, DI) -- see NestJS example below
│   └── NO → Use Express 5 (minimal, fast to learn) -- see Express example below
└── DEFAULT → Start with Express 5, migrate highest-traffic endpoints first, add NestJS when team is ready
```

## Step-by-Step Guide

### 1. Audit the PHP codebase and map endpoints

Catalog every PHP entry point, database query, session usage, and external API call. This converts a vague migration into discrete, estimable tasks. [src3]

```bash
# Count PHP files and entry points
find . -name '*.php' | wc -l
grep -rn 'function\s' --include='*.php' | wc -l

# Find all route definitions (Laravel example)
grep -rn "Route::" --include='*.php' routes/ | wc -l

# Find all database queries
grep -rn 'query\|prepare\|execute\|PDO\|DB::' --include='*.php' | wc -l

# Find session usage
grep -rn '\$_SESSION\|session_start\|Session::' --include='*.php' | wc -l
```

**Verify**: You have a spreadsheet/list of every endpoint, its HTTP method, database tables touched, and whether it reads/writes session state.

### 2. Set up the Node.js project alongside PHP

Initialize the Node.js application without removing the PHP app. Both run in parallel behind a reverse proxy. [src1, src2]

```bash
# Express 5 setup
mkdir node-backend && cd node-backend
npm init -y
npm install express@5 cors helmet dotenv
npm install -D typescript @types/express @types/node ts-node nodemon

# Create tsconfig.json
npx tsc --init --outDir dist --rootDir src --strict true --esModuleInterop true

# Or NestJS 11 setup
npx @nestjs/cli new node-backend --strict --package-manager npm
```

```typescript
// src/app.ts -- minimal Express 5 server
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import 'dotenv/config';

const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());

app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Node.js server running on port ${PORT}`));
```

**Verify**: `curl http://localhost:3000/health` returns `{"status":"ok"}`.

### 3. Configure reverse proxy for gradual traffic shifting

Set up Nginx (or your existing load balancer) to route traffic to either PHP or Node.js based on the URL path. New/migrated endpoints go to Node.js; everything else stays on PHP. This is the strangler fig pattern in action. [src3]

```nginx
# /etc/nginx/sites-available/myapp
upstream php_backend {
    server 127.0.0.1:9000;  # PHP-FPM
}

upstream node_backend {
    server 127.0.0.1:3000;  # Node.js
}

server {
    listen 80;
    server_name myapp.com;

    # Migrated endpoints -> Node.js
    location /api/v2/ {
        proxy_pass http://node_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Everything else -> PHP (legacy)
    location / {
        try_files $uri $uri/ /index.php?$query_string;
        fastcgi_pass php_backend;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
```

**Verify**: `curl http://myapp.com/api/v2/health` hits Node.js; `curl http://myapp.com/dashboard` hits PHP.

### 4. Migrate the database layer

Replace PHP PDO calls with Knex.js (SQL query builder, closest to PDO) or Prisma 6 (ORM with type safety). Keep the same database -- no need to migrate data. [src7]

```typescript
// src/db.ts -- Knex setup (familiar for PDO users)
import knex from 'knex';
import 'dotenv/config';

export const db = knex({
  client: 'mysql2',  // or 'pg' for PostgreSQL
  connection: {
    host: process.env.DB_HOST,
    port: Number(process.env.DB_PORT) || 3306,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
  },
  pool: { min: 2, max: 10 },
});

// PHP PDO equivalent: $stmt = $pdo->prepare('SELECT * FROM users WHERE active = ?');
// Node.js Knex equivalent:
const activeUsers = await db('users').where('active', true).select('*');

// PHP: $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)')->execute([$name, $email]);
// Node.js:
await db('users').insert({ name: 'Alice', email: 'alice@example.com' });
```

**Verify**: `node -e "require('./src/db').db('users').count('* as count').then(console.log)"` returns a row count matching your PHP app's database.

### 5. Convert authentication and session management

Replace PHP sessions with JWT tokens or `express-session` backed by Redis. JWT is preferred for API-first architectures; `express-session` is the closest PHP-like experience. [src4, src5]

```typescript
// src/middleware/auth.ts -- JWT authentication
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

const JWT_SECRET = process.env.JWT_SECRET!;

// PHP equivalent: session_start(); if (!$_SESSION['user']) redirect('/login');
export function authenticate(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const payload = jwt.verify(token, JWT_SECRET) as { userId: number; role: string };
    (req as any).user = payload;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Login endpoint -- replaces PHP's session_start() + $_SESSION['user'] = $user
export async function login(req: Request, res: Response) {
  const { email, password } = req.body;
  const user = await db('users').where('email', email).first();

  if (!user || !(await bcrypt.compare(password, user.password_hash))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { userId: user.id, role: user.role },
    JWT_SECRET,
    { expiresIn: '24h' }
  );
  res.json({ token, user: { id: user.id, name: user.name } });
}
```

**Verify**: `curl -X POST /api/v2/login -d '{"email":"test@test.com","password":"pass"}' -H 'Content-Type: application/json'` returns a JWT token. Subsequent requests with `Authorization: Bearer <token>` pass authentication.

### 6. Migrate routes and business logic

Convert PHP controller methods to Express route handlers or NestJS controllers. Move business logic into service modules, not directly in route handlers. [src4, src6]

```typescript
// src/routes/users.ts -- Express route (replaces PHP UserController)
import { Router } from 'express';
import { db } from '../db';
import { authenticate } from '../middleware/auth';

const router = Router();

// PHP: public function index() { $users = User::all(); return view('users.index', compact('users')); }
// Node.js: API-only, returns JSON
router.get('/', authenticate, async (req, res) => {
  const page = Number(req.query.page) || 1;
  const limit = Number(req.query.limit) || 20;
  const offset = (page - 1) * limit;

  const [users, [{ count }]] = await Promise.all([
    db('users').select('id', 'name', 'email', 'created_at').limit(limit).offset(offset),
    db('users').count('* as count'),
  ]);

  res.json({
    data: users,
    meta: { page, limit, total: Number(count) },
  });
});

// PHP: public function store(Request $request) { $user = User::create($request->validated()); }
router.post('/', authenticate, async (req, res) => {
  const { name, email, password } = req.body;

  // Validation (use Zod, Joi, or class-validator in production)
  if (!name || !email || !password) {
    return res.status(400).json({ error: 'name, email, and password are required' });
  }

  const hash = await bcrypt.hash(password, 12);
  const [id] = await db('users').insert({ name, email, password_hash: hash });
  const user = await db('users').where('id', id).first();

  res.status(201).json({ data: user });
});

export default router;
```

**Verify**: `curl /api/v2/users -H 'Authorization: Bearer <token>'` returns the same user list as the PHP endpoint.

### 7. Set up error handling, logging, and monitoring

Replace PHP's error handling with Express async error middleware. Add structured logging (pino/winston) and health checks. [src2, src3]

```typescript
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import pino from 'pino';

const logger = pino({ level: process.env.LOG_LEVEL || 'info' });

// Express 5 automatically catches async errors -- no try/catch wrapper needed
// PHP equivalent: set_exception_handler() + set_error_handler()
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  logger.error({
    err,
    method: req.method,
    url: req.url,
    ip: req.ip,
  }, 'Unhandled error');

  const status = (err as any).status || 500;
  res.status(status).json({
    error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
  });
}

// Register last in the middleware chain
// app.use(errorHandler);
```

**Verify**: Trigger an intentional error -- the response should be a structured JSON error, and the log file should contain the stack trace.

## Code Examples

### Express 5 (TypeScript): Complete REST API replacing a PHP CRUD backend

> Full script: [express-5-typescript-complete-rest-api-replacing-a.ts](scripts/express-5-typescript-complete-rest-api-replacing-a.ts) (59 lines)

```typescript
// Input:  A PHP Laravel-style CRUD controller with routes, validation, and DB queries
// Output: Equivalent Express 5 + Knex.js REST API with full error handling
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
# ... (see full script)
```

### NestJS 11 (TypeScript): Structured migration with modules, controllers, services

> Full script: [nestjs-typescript-structured-migration-with-module.ts](scripts/nestjs-typescript-structured-migration-with-module.ts) (53 lines)

```typescript
// Input:  A PHP Symfony/Laravel controller with DI, services, and repositories
// Output: NestJS equivalent with the same architectural patterns
// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
# ... (see full script)
```

### Database Migration: Converting PHP/PDO schema management to Knex migrations

> Full script: [database-migration-converting-php-pdo-schema-manag.ts](scripts/database-migration-converting-php-pdo-schema-manag.ts) (33 lines)

```typescript
// Input:  PHP migration files (Laravel/Phinx style)
// Output: Knex.js migration files
// migrations/20260217_create_users_table.ts
import { Knex } from 'knex';
// PHP equivalent:
# ... (see full script)
```

## Anti-Patterns

### Wrong: Blocking the event loop with synchronous operations

```javascript
// BAD -- Synchronous file read blocks ALL concurrent requests
// PHP does this safely because each request has its own process
const fs = require('fs');

app.get('/report', (req, res) => {
  const data = fs.readFileSync('/path/to/large-file.csv', 'utf8');  // Blocks event loop!
  const parsed = parseCSV(data);  // CPU-intensive -- blocks further!
  res.json(parsed);
});
```

### Correct: Use async I/O and worker threads for CPU work

```javascript
// GOOD -- Async I/O + worker thread for CPU-bound work
const fs = require('fs/promises');
const { Worker } = require('worker_threads');

app.get('/report', async (req, res) => {
  const data = await fs.readFile('/path/to/large-file.csv', 'utf8');  // Non-blocking
  const parsed = await runInWorker('parseCSV', data);  // Off main thread
  res.json(parsed);
});

function runInWorker(fn, data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./workers/csv-worker.js', { workerData: { fn, data } });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}
```

### Wrong: Using PHP-style global state for request data

```javascript
// BAD -- Global variable shared across ALL concurrent requests
// In PHP, each request has isolated memory. In Node.js, globals are shared.
let currentUser = null;

app.use((req, res, next) => {
  currentUser = getUserFromToken(req.headers.authorization);  // Race condition!
  next();
});

app.get('/profile', (req, res) => {
  res.json(currentUser);  // May return WRONG user under concurrent load
});
```

### Correct: Attach request-scoped data to the request object

```javascript
// GOOD -- Data scoped to the request object, safe under concurrency
app.use((req, res, next) => {
  req.currentUser = getUserFromToken(req.headers.authorization);
  next();
});

app.get('/profile', (req, res) => {
  res.json(req.currentUser);  // Always correct -- scoped to this request
});
```

### Wrong: No connection pooling (creating a new DB connection per query)

```javascript
// BAD -- PHP-style: new connection per request (PDO reconnects automatically)
// In Node.js, this exhausts connections under load
app.get('/users', async (req, res) => {
  const connection = await mysql.createConnection({
    host: 'localhost', user: 'root', database: 'mydb'
  });
  const [rows] = await connection.execute('SELECT * FROM users');
  await connection.end();  // Connection created and destroyed every request
  res.json(rows);
});
```

### Correct: Use a connection pool (Knex, Prisma, or mysql2 pool)

```javascript
// GOOD -- Connection pool reuses connections across requests
import knex from 'knex';

const db = knex({
  client: 'mysql2',
  connection: process.env.DATABASE_URL,
  pool: { min: 2, max: 10 },  // Reuses connections
});

app.get('/users', async (req, res) => {
  const users = await db('users').select('*');  // Uses pool automatically
  res.json(users);
});
```

### Wrong: Trusting PHP's error suppression patterns in Node.js

```javascript
// BAD -- Silently swallowing errors like PHP's @ operator
app.get('/data', async (req, res) => {
  try {
    const data = await fetchExternalAPI();
    res.json(data);
  } catch (e) {
    // Silent catch -- equivalent to PHP's @file_get_contents()
    // No logging, no monitoring, no alerting
    res.json({ data: [] });
  }
});
```

### Correct: Log errors, return appropriate status codes

```javascript
// GOOD -- Structured error handling with logging
app.get('/data', async (req, res) => {
  try {
    const data = await fetchExternalAPI();
    res.json(data);
  } catch (error) {
    logger.error({ err: error, url: req.url }, 'External API call failed');
    res.status(502).json({
      error: 'Upstream service unavailable',
      retryAfter: 30,
    });
  }
});
```

### Wrong: Using `eval()` or dynamic requires for PHP-style dynamic routing

```javascript
// BAD -- Dynamic require based on user input (equivalent to PHP's include($_GET['page']))
app.get('/:page', (req, res) => {
  const module = require(`./pages/${req.params.page}`);  // Path traversal vulnerability!
  res.send(module.render());
});
```

### Correct: Explicit route registration with an allowlist

```javascript
// GOOD -- Explicit route mapping
const pages = {
  home: () => import('./pages/home'),
  about: () => import('./pages/about'),
  contact: () => import('./pages/contact'),
};

app.get('/:page', async (req, res) => {
  const loader = pages[req.params.page];
  if (!loader) return res.status(404).json({ error: 'Page not found' });
  const module = await loader();
  res.send(module.render());
});
```

## Common Pitfalls

- **Blocking the event loop with CPU-intensive code**: PHP handles this transparently (one process per request), but Node.js serves ALL requests on a single thread. A 500ms CPU operation blocks every concurrent user. Fix: Use `worker_threads` for CPU work, or offload to a message queue (BullMQ, RabbitMQ). [src1]
- **Assuming each request has isolated memory**: PHP globals (`$_SESSION`, `$GLOBALS`) are per-request. In Node.js, module-level variables are shared across ALL concurrent requests. Fix: Never store request-scoped data in module-level variables -- use `req.locals` or `res.locals`. [src5]
- **Mixing callbacks and Promises**: PHP doesn't have callbacks. New Node.js developers mix `callback(err, data)` with `async/await`, creating error-handling gaps. Fix: Use `util.promisify()` to convert callback APIs, or use only `async/await`. Express 5 natively catches async rejections. [src2]
- **Not setting up process management**: PHP-FPM manages worker processes automatically. Node.js needs PM2, Docker, or `node --cluster` for zero-downtime restarts and multi-core usage. Fix: `npm install -g pm2 && pm2 start app.js -i max`. [src4]
- **Ignoring the callback queue and microtask queue ordering**: PHP executes top-to-bottom. Node.js has event loop phases (timers, I/O, microtasks). Fix: Read the Node.js event loop documentation and use `setImmediate()` vs `process.nextTick()` intentionally. [src1]
- **Migrating everything at once**: Big-bang rewrites fail. Fix: Use the strangler fig pattern -- migrate one endpoint at a time behind a reverse proxy. Ship each migration before starting the next. [src3]
- **Forgetting to handle uncaught exceptions**: PHP crashes per-request; Node.js crashes the entire server. Fix: Add `process.on('uncaughtException')` and `process.on('unhandledRejection')` handlers, plus use PM2 for auto-restart. [src1]
- **Underestimating PHP 8.4 improvements**: PHP 8.4 has property hooks, JIT improvements, and fibers for async-like patterns. If the only driver is "PHP is slow," benchmark first -- PHP 8.4 can be 2x faster than PHP 7.4. Migration may not be necessary. [src5]

## Diagnostic Commands

```bash
# Check Node.js version (target 24.x = Active LTS Krypton, 22.x = Maintenance LTS)
node --version

# Verify npm packages installed correctly
npm ls --depth=0

# Test database connection from Node.js
node -e "require('knex')({client:'pg',connection:process.env.DATABASE_URL}).raw('SELECT 1').then(()=>console.log('DB OK')).catch(console.error)"

# Check for event loop blocking (should show <10ms delay)
node -e "setInterval(() => { const start = Date.now(); setImmediate(() => console.log('lag:', Date.now() - start, 'ms')); }, 1000)"

# Run Express app with auto-reload during development
npx nodemon --exec ts-node src/app.ts

# Check PM2 process status in production
pm2 status && pm2 logs --lines 50

# Verify Nginx proxy routing
curl -I http://localhost/api/v2/health  # Should show Node.js response headers
curl -I http://localhost/dashboard       # Should show PHP response headers

# Compare response parity between PHP and Node.js endpoints
diff <(curl -s http://localhost/api/v1/users) <(curl -s http://localhost/api/v2/users)

# Profile Node.js app for event loop delays
node --prof src/app.js  # Creates isolate-*.log, then: node --prof-process isolate-*.log
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Node.js 24.x (2025) | **Active LTS (Krypton)** since Oct 2025 | OpenSSL 3.5 (security level 2, no <2048-bit RSA), V8 13.x, native TypeScript type-stripping stable, npm v11 (65% faster installs) | Recommended runtime for new migrations. EOL April 2028. [src8] |
| Node.js 22.x (2024) | Maintenance LTS (Jod) | `require()` for ESM (experimental), WebSocket API built-in | Stable V8 12.x, top-level await, built-in fetch stable, Watch Mode stable |
| Node.js 20.x (2023) | Maintenance | Permission model, test runner stable | Single executable apps, stable fetch, stable WebCrypto |
| Node.js 18.x (2022) | EOL (Apr 2025) | Built-in fetch (experimental), test runner | Minimum for Express 5 -- upgrade to 22+ or 24 immediately |
| Express 5.1 (2025) | Current | Async error handling, dropped `app.del()`, path params syntax | Automatic promise rejection forwarding, no try/catch wrappers needed [src2] |
| Express 4.x (2014) | Maintenance | -- | Upgrade to 5.x: remove `app.del()`, update path regex patterns |
| NestJS 12 (alpha, Q3 2026) | Pre-release | **Full ESM migration** (drops CommonJS), Vitest replaces Jest, oxlint replaces ESLint, Rspack replaces Webpack, native Standard Schema validation | Plan migration timing if starting fresh — alpha-4 on npm `next` tag. [src9] |
| NestJS 11 (2025) | Current | JSON logging built-in, IntrinsicException, improved startup perf | Requires Node.js 18+ (recommend 22 or 24), Apollo Server v4 for GraphQL [src6] |
| NestJS 10 (2023) | Maintenance | SWC compiler default, Redis v4 | Requires Node.js 16+ |
| Prisma 6.x (2025) | Current | Query Compiler replaces Rust engine, ESM support | Leaner architecture, schema splitting GA, min Node.js 18.18+ |
| Knex 3.x (2023) | Current | ESM-first, dropped Node <14 | Use `type: "module"` in package.json or `.mjs` extensions [src7] |
| PHP 8.4 (2024) | Current (source side) | Property hooks, new DOM API | JIT improvements, 2x faster than PHP 7.4 on some workloads |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| I/O-heavy app (API gateway, real-time, streaming) | CPU-heavy computation (video encoding, ML inference) | Go, Rust, or keep PHP with worker queues |
| Team knows JavaScript/TypeScript well | Team is highly productive in PHP and no pain points | Stay on PHP 8.4 (JIT, fibers, property hooks) |
| Building microservices or API-first architecture | Simple CMS or blog (WordPress, Drupal) | Keep PHP + WordPress, or use headless CMS |
| Need real-time features (WebSocket, SSE) | App is a traditional server-rendered MVC | Laravel/Symfony with Livewire/Hotwire |
| Unifying frontend and backend in one language | PHP codebase is small (<5K LOC) and stable | Incremental PHP modernization (PHP 8.4, Composer, PSR-15) |
| High-concurrency API (>10K concurrent connections) | Batch processing or cron jobs only | Keep PHP scripts, use Node.js only for the API layer |

## Important Caveats

- Node.js is single-threaded by default. For multi-core utilization, use `cluster` module, PM2 in cluster mode, or Docker with multiple containers. PHP-FPM handles this transparently with its worker pool.
- Express 5.x requires Node.js 18+. It automatically forwards rejected promises to error-handling middleware -- no need for the `express-async-errors` package that was required in Express 4.
- PHP's `include`/`require` execute at runtime; Node.js `require()` caches modules after first load. This means configuration changes require a server restart (or use `dotenv` with a reload mechanism).
- Session handling is fundamentally different: PHP sessions use server-side files by default; Node.js has no built-in session mechanism. Use `express-session` with a Redis store for production, or switch to JWT for stateless authentication.
- PHP's `PDO::ERRMODE_EXCEPTION` throws on query errors. Knex.js always returns Promises that reject on error -- ensure every database call has proper `.catch()` or is wrapped in `try/catch`.
- File upload handling differs significantly: PHP populates `$_FILES` automatically. In Node.js, you need middleware like `multer` (multipart) or `busboy` -- the raw request body is a stream, not a pre-parsed array.
- Fastify (5.x) is a viable alternative to Express with 2-3x higher throughput in benchmarks. Consider it for new high-traffic services during migration. It offers built-in schema validation, TypeScript support, and a plugin architecture.

## Related Units

- [PHP to Python Migration](/software/migrations/php-to-python/2026)
- [PHP to Go Migration](/software/migrations/php-to-go/2026)
- [Rails to Node.js Migration](/software/migrations/rails-to-nodejs/2026)
- [Express to NestJS Migration](/software/migrations/express-to-nestjs/2026)
- [Express to Fastify Migration](/software/migrations/express-to-fastify/2026)
- [jQuery to React Migration](/software/migrations/jquery-to-react/2026)
