nodejs-development · v1.0.0 · 2026-09-07 · sha256 e6fb6d522b4d017e
nodejs-development v1.0.0A
Immutable. This exact content is served forever at /api/v1/blob/e6fb6d522b4d017e.
---
name: "nodejs-development"
description: "Use when building, debugging, or optimizing Node.js applications — Fastify servers, TypeScript with type stripping, Node.js core internals, OAuth 2.0/2.1 auth flows, advanced TypeScript types, or diagnosing performance/crash issues. Covers the full Node.js stack from framework patterns (Fastify plugins, routes, schemas, hooks) through internals (V8, libuv, N-API, C++ addons) to production concerns (graceful shutdown, caching, streams, testing, error handling)."
version: "1.0.0"
author: "JPeetz (adapted from Matteo Collina's skills: mcollina/skills)"
license: "MIT"
metadata:
hermes:
tags:
- nodejs
- fastify
- typescript
- backend
- api
- server
- type-stripping
- streams
- caching
- v8
- libuv
- n-api
- oauth
- testing
- performance
- async
related_skills:
- api-design-first
- graphql-api-development
- github-code-review
- test-driven-development
- codebase-inspection
- systematic-debugging
---
# Node.js Development — Meta-Skill
A comprehensive collection of Node.js development patterns distilled from Matteo Collina's (Fastify creator, Node.js core contributor) personal skills. Covers **Fastify** application architecture, **Node.js** best practices (type stripping, streams, caching, error handling, testing), **Node.js core internals** (V8, libuv, N-API, C++ addons), **OAuth 2.0/2.1** authorization flows, and **advanced TypeScript** type patterns.
> **Source:** Adapted from [mcollina/skills](https://github.com/mcollina/skills) — 11 skills by Matteo Collina.
---
## Table of Contents
1. [When to Use](#when-to-use)
2. [Section A: Node.js Best Practices](#section-a-nodejs-best-practices)
3. [Section B: Fastify Application Architecture](#section-b-fastify-application-architecture)
4. [Section C: Node.js Core Internals](#section-c-nodejs-core-internals)
5. [Section D: OAuth 2.0/2.1 with Fastify](#section-d-oauth-2021-with-fastify)
6. [Section E: Advanced TypeScript Types](#section-e-advanced-typescript-types)
7. [Pitfalls](#pitfalls)
8. [Verification](#verification)
---
## When to Use
Trigger this skill when any of these appear in the prompt:
| Signal | Relevant Section |
|--------|-----------------|
| "Fastify", "Fastify plugin", "Fastify routes", "Fastify hooks" | B |
| "Node 22", "type stripping", "strip types", "native TypeScript", ".ts without build" | A |
| "streams", "pipeline", "CSV/ETL", "backpressure", "large file processing" | A |
| "graceful shutdown", "close-with-grace", "SIGTERM", "SIGINT" | A |
| "flaky tests", "node --test", "test timeout", "process did not exit" | A |
| "V8", "libuv", "event loop", "node-gyp", "N-API", "NAN", "C++ addon", "segfault" | C |
| "OAuth", "authorization code", "PKCE", "refresh token rotation", "JWT validation" | D |
| "conditional types", "infer", "mapped types", "branded types", "opaque types" | E |
| "any type", "remove any", "strict TypeScript" | E |
| "tsc --noEmit", "type error", "compiler error" | E |
**General Node.js development** (error handling, logging, performance, profiling, environment config) uses Section A.
---
## Section A: Node.js Best Practices
### A1. TypeScript with Type Stripping (Node 22.6+)
Use **type stripping** instead of build tools (ts-node, tsx). Node runs `.ts` files directly by removing type annotations without transpilation.
**Requirements:**
- Use `import type` for type-only imports
- Use const objects instead of `enum`
- Avoid `namespace` and parameter properties
- Use `.ts` extensions in import paths
```ts
// greet.ts — valid type-stripped file
import type { IncomingMessage } from 'node:http';
const greet = (name: string): string => `Hello, ${name}!`;
console.log(greet('world'));
```
```bash
node greet.ts # runs directly, no build step
```
**tsconfig.json for type stripping:**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
}
}
```
### A2. Error Handling
Use `@fastify/create-error` for typed, code-bearing errors:
```ts
import createError from '@fastify/create-error';
const NotFoundError = createError('NOT_FOUND', '%s not found', 404);
const ValidationError = createError('VALIDATION_ERROR', '%s', 400);
throw new NotFoundError('User'); // → 404, code: NOT_FOUND
throw new ValidationError('Email required'); // → 400, code: VALIDATION_ERROR
```
**Minimal zero-dependency pattern:**
```ts
interface AppErrorOptions { code: string; statusCode?: number; cause?: Error; }
function createAppError(message: string, opts: AppErrorOptions): Error {
const err = new Error(message, { cause: opts.cause });
(err as any).code = opts.code;
(err as any).statusCode = opts.statusCode ?? 500;
Error.captureStackTrace(err, createAppError);
return err;
}
function notFound(resource: string) {
return createAppError(`${resource} not found`, { code: 'NOT_FOUND', statusCode: 404 });
}
```
**Operational vs programmer errors:** Classify errors at the boundary. Operational errors (failed network call, file not found) should be handled gracefully. Programmer errors (TypeError, ReferenceError) should crash or restart.
**Global async error handlers:**
```ts
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
// Log and crash hard — these are programmer errors
process.exitCode = 1;
});
process.on('uncaughtException', (err, origin) => {
console.error('Uncaught Exception:', err, origin);
// Perform emergency cleanup, then exit
process.exit(1);
});
```
### A3. Streams
**Always use `pipeline()` from `node:stream/promises`** — never bare `.pipe()`:
```ts
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
await pipeline(
createReadStream('input.csv'),
createGzip(),
createWriteStream('output.csv.gz')
);
```
**Async generator transforms** (preferred over `Transform` class):
```ts
async function* toUpperCase(source: AsyncIterable<Buffer>): AsyncGenerator<string> {
for await (const chunk of source) {
yield chunk.toString().toUpperCase();
}
}
await pipeline(createReadStream(input), toUpperCase, createWriteStream(output));
```
**CSV/ETL pattern** — pipeline + async transform + deduplicated enrichment:
```ts
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createCache } from 'async-cache-dedupe';
const cache = createCache({ ttl: 60, stale: 5, storage: { type: 'memory' } });
cache.define('lookupUser', async (id: string) => {
const res = await fetch(`https://api.internal/users/${id}`);
return res.json();
});
async function* enrichRows(source: AsyncIterable<Buffer>): AsyncGenerator<string> {
for await (const chunk of source) {
const row = JSON.parse(chunk.toString());
const user = await cache.lookupUser(row.userId); // deduped
yield JSON.stringify({ ...row, user });
}
}
await pipeline(createReadStream('data.ndjson'), enrichRows, createWriteStream('enriched.ndjson'));
```
**Pitfall:** Pipelines are lazy — without a consumer they never run. Always `await pipeline(...)` and never omit the destination stream.
### A4. Caching
| Library | Use Case | Key Feature |
|---------|----------|-------------|
| `lru-cache` | Bounded in-memory cache (max N entries) | Fast, predictable eviction |
| `async-cache-dedupe` | Deduplicate concurrent async calls | Stale-while-revalidate, dedup |
```ts
// lru-cache: bounded, in-memory
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({ max: 500, ttl: 1000 * 60 * 5 });
cache.set('key', value);
const val = cache.get('key');
// async-cache-dedupe: deduplicates concurrent requests
import { createCache } from 'async-cache-dedupe';
const dedupe = createCache({ ttl: 60, stale: 10 });
dedupe.define('fetchPlan', async (id: string) => api.getPlan(id));
// 100 concurrent calls → 1 actual request
const results = await Promise.all(ids.map(id => dedupe.fetchPlan(id)));
```
### A5. Graceful Shutdown
Use `close-with-grace` (from the Fastify team):
```ts
import closeWithGrace from 'close-with-grace';
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
});
server.listen(3000);
closeWithGrace({ delay: 10000 }, async ({ signal, err }) => {
if (err) console.error('Shutdown error:', err);
console.log(`${signal} received, closing server...`);
// 1. Stop accepting new connections
await new Promise<void>((resolve) => server.close(() => resolve()));
// 2. Drain in-flight requests (the server.close callback handles this)
// 3. Close external connections
await db.end();
console.log('Shutdown complete');
});
```
**Signal handling order:** SIGTERM/SIGINT → stop accepting new work → drain in-flight → close DB/cache → exit.
### A6. Testing with Node.js Built-in Test Runner (node:test)
```ts
import { describe, it, before, after, mock } from 'node:test';
import assert from 'node:assert';
describe('UserService', () => {
let service: UserService;
before(() => { service = new UserService(); });
it('should create a user', async (t) => {
const user = await service.create({ name: 'John' });
assert.strictEqual(user.name, 'John');
assert.ok(user.id);
});
it('should reject invalid input', async (t) => {
await assert.rejects(
() => service.create({ name: '' }),
{ message: 'Name is required' }
);
});
});
```
**Mocking with test context:**
```ts
it('should send email', async (t) => {
const sendMock = t.mock.fn(async () => ({ success: true }));
const service = new EmailService({ send: sendMock });
await service.sendWelcome('user@example.com');
assert.strictEqual(sendMock.mock.calls.length, 1);
});
```
**Fastify `inject()` for integration testing:**
```ts
import Fastify from 'fastify';
import { test } from 'node:test';
import assert from 'node:assert';
import myApp from '../app.js';
test('GET /health returns ok', async () => {
const app = Fastify();
await app.register(myApp);
await app.ready();
const response = await app.inject({ method: 'GET', url: '/health' });
assert.strictEqual(response.statusCode, 200);
assert.deepStrictEqual(response.json(), { status: 'ok' });
});
```
**Diagnosing flaky tests:**
1. Isolate the test with `--test-only` (`test.only()` in the file)
2. Run with `--test-timeout=5000` to surface hangs
3. Check for shared state, timer dependencies, or async teardown order
4. Use `--test-reporter=spec` for verbose output
5. Run individual files: `for f in src/**/*.test.ts; do echo "Running: $f"; timeout 30s node --test "$f" || echo "TIMEOUT: $f"; done`
**Diagnosing stuck processes** (tests that "do not exit"):
1. Isolate the file/test
2. Run with explicit timeout and reporter
3. Find open handles with `SIGUSR1` (requires `why-is-node-running`)
4. Patch deterministic teardown in every resource-creation scope
### A7. Async Patterns
- **Always** prefer `async/await` over raw `.then()` chains
- Use `Promise.allSettled()` when you need results from all promises even if some reject
- Use `AbortController` + `AbortSignal` for cancellable operations
- For concurrent bounded work: `p-limit` or `Promise.allSettled` with chunking
```ts
// Cancellable fetch with AbortController
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const ac = new AbortController();
const timeout = setTimeout(() => ac.abort(), ms);
try {
return await fetch(url, { signal: ac.signal });
} finally {
clearTimeout(timeout);
}
}
```
### A8. Profiling
```bash
# CPU profile
node --cpu-prof --cpu-prof-dir=./profiles app.js
node --prof-process isolate-*.log > processed.txt
# V8 optimization tracing
node --trace-opt --trace-deopt app.js
# Checkpoint: confirm no unexpected deoptimizations
# Event loop lag
node --trace-event-categories v8,node,node.async_hooks app.js
# Heap snapshot (open chrome://inspect)
node --inspect app.js
```
### A9. Environment Configuration
- Use `process.env` with validated defaults — never use `dotenv` in production
- Parse and validate at startup, not at point of use
- Use `@fastify/env` (see Section B) for Fastify apps
```ts
function env(key: string, fallback?: string): string {
const val = process.env[key] ?? fallback;
if (val === undefined) throw new Error(`Missing required env: ${key}`);
return val;
}
const config = {
port: parseInt(env('PORT', '3000'), 10),
dbUrl: env('DATABASE_URL'),
logLevel: env('LOG_LEVEL', 'info'),
};
Object.freeze(config);
```
---
## Section B: Fastify Application Architecture
### B1. Quick Start
```ts
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get('/health', async (request, reply) => {
return { status: 'ok' };
});
const start = async () => {
await app.listen({ port: 3000, host: '0.0.0.0' });
};
start();
```
### B2. Plugin System
Fastify provides **automatic encapsulation** — each `app.register()` creates an isolated context.
```ts
// Encapsulated plugin — decorators NOT exposed to parent
app.register(async function childPlugin(fastify) {
fastify.decorate('privateUtil', () => 'only available here');
fastify.get('/child', async function (req, reply) {
return this.privateUtil();
});
});
```
**`fastify-plugin` to break encapsulation** (share decorators with parent context):
```ts
import fp from 'fastify-plugin';
export default fp(async function dbPlugin(fastify, opts) {
const db = await connect(opts.connectionString);
fastify.decorate('db', db);
fastify.addHook('onClose', async () => { await db.close(); });
}, { name: 'database-plugin', dependencies: [] });
```
### B3. JSON Schema Validation with TypeBox
**Always use TypeBox** for type-safe schemas:
```ts
import { Type, type Static } from '@sinclair/typebox';
const CreateUserBody = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' }),
age: Type.Optional(Type.Integer({ minimum: 0 })),
});
type CreateUserBodyType = Static<typeof CreateUserBody>;
app.post<{ Body: CreateUserBodyType }>('/users', {
schema: { body: CreateUserBody },
}, async (req, reply) => {
// req.body is fully typed
const user = await createUser(req.body);
return user;
});
```
**Request validates:** `body`, `querystring`, `params`, `headers`. **Response serializes** via `response` schema.
### B4. Request Lifecycle (Hooks)
Order of execution: `onRequest` → `preParsing` → `preValidation` → `preHandler` → handler → `preSerialization` → `onSend` → `onResponse`
```ts
// Global hook — applies to all routes
app.addHook('onRequest', async (request, reply) => {
// Common: rate limiting, request logging, CORS pre-flight
});
// Scoped hook — applies only to routes in this plugin
app.register(async function scopedRoutes(fastify) {
fastify.addHook('onRequest', async (req, reply) => {
// Only affects routes registered inside this plugin
});
});
```
### B5. Authentication
```ts
import jwt from '@fastify/jwt';
app.register(jwt, { secret: process.env.JWT_SECRET! });
app.addHook('onRequest', async (request, reply) => {
try {
await request.jwtVerify();
} catch (err) {
reply.send(err);
}
});
app.get('/me', async (request) => {
return request.user; // decoded JWT payload
});
```
### B6. Testing with inject()
```ts
import Fastify from 'fastify';
import myApp from '../app.js';
const app = Fastify();
await app.register(myApp);
await app.ready();
const res = await app.inject({
method: 'POST',
url: '/users',
payload: { name: 'John', email: 'john@test.com' },
});
assert.strictEqual(res.statusCode, 201);
assert.deepStrictEqual(res.json().name, 'John');
```
### B7. Logging with Pino
Fastify uses Pino by default. Control via `logger` option:
```ts
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
},
});
```
**Per-request logs:** `request.log.info('processing order')` — automatically includes `reqId`.
### B8. CORS and Security Headers
```ts
import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
await app.register(cors, {
origin: ['https://app.example.com'],
credentials: true,
});
await app.register(helmet);
```
### B9. Error Handling in Fastify
```ts
// Global error handler
app.setErrorHandler(async (error, request, reply) => {
const statusCode = error.statusCode ?? error.status ?? 500;
request.log.error({ err: error }, 'Request error');
return reply.status(statusCode).send({
error: {
code: error.code ?? 'INTERNAL_ERROR',
message: statusCode >= 500 ? 'Internal server error' : error.message,
},
});
});
```
### B10. Full Lifecycle Recommendations
| Scenario | Reading Order |
|----------|--------------|
| New to Fastify | Plugins → Routes → Schemas |
| Adding auth | Plugins → Hooks → Authentication |
| Performance | Schemas → Serialization → Performance |
| Testing | Routes → Testing |
| Production | Logging → Configuration → Deployment |
---
## Section C: Node.js Core Internals
### C1. V8 Engine
**Garbage Collection:**
- **Scavenger** (young generation): fast, frequent. Objects that survive 2+ collections move to old generation.
- **Mark-Sweep** (old generation): marks live objects, sweeps dead ones. Triggers when old gen runs out of space.
- **Mark-Compact** (old gen): like Mark-Sweep but also compacts memory to reduce fragmentation.
**Hidden Classes and Inline Caching:**
- Adding properties to an object out of order creates different hidden classes → deoptimization
- Always initialize object properties in consistent order
**TurboFan JIT:**
- Functions become "hot" after ~1000 calls → TurboFan optimizes them
- Deoptimization happens when assumptions change (e.g., monomorphic call becomes polymorphic)
```bash
# Trace V8 optimization
node --trace-opt --trace-deopt app.js
# CPU profiling
node --prof app.js
node --prof-process isolate-*.log > processed.txt
```
### C2. libuv Event Loop
**Phases** (in order each tick):
1. **Timers** — `setTimeout`, `setInterval` callbacks
2. **Pending callbacks** — I/O callbacks deferred from previous cycle
3. **Idle, prepare** — internal use
4. **I/O poll** — waits for I/O events (blocking most of the time)
5. **Check** — `setImmediate` callbacks
6. **Close callbacks** — `close` events (e.g., socket.on('close'))
```bash
# Detect event loop lag
node --trace-event-categories v8,node,node.async_hooks app.js
# Thread pool (default 4 threads)
UV_THREADPOOL_SIZE=8 node app.js
```
**Key rules:**
- `setTimeout(cb, 0)` runs in **timers phase** (after I/O poll)
- `setImmediate(cb)` runs in **check phase** (after I/O poll, before next timers)
- In I/O callbacks, `setImmediate` always fires before `setTimeout(cb, 0)`
- Never block the event loop with synchronous CPU work — use worker threads
### C3. N-API and C++ Addons
```cpp
// Basic N-API addon: addon.cpp
#include <node_api.h>
napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
double a, b;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
napi_value result;
napi_create_double(env, a + b, &result);
return result;
}
napi_value Init(napi_env env, napi_value exports) {
napi_value fn;
napi_create_function(env, nullptr, 0, Add, nullptr, &fn);
napi_set_named_property(env, exports, "add", fn);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
```
**binding.gyp:**
```json
{
"targets": [{
"target_name": "addon",
"sources": ["addon.cpp"]
}]
}
```
**node-addon-api** (C++ wrapper, preferred for new code):
```cpp
#include <napi.h>
Napi::Number Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
double a = info[0].As<Napi::Number>().DoubleValue();
double b = info[1].As<Napi::Number>().DoubleValue();
return Napi::Number::New(env, a + b);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(addon, Init)
```
**Segfault in native addon — decision tree:**
1. Reproduce with `node --napi-modules` → run `gdb`, capture `bt`
2. Does `bt` point to a V8 handle scope issue? → Check `HandleScope` usage
3. Points to a libuv callback? → Inspect async handle lifetime and `uv_close()` sequencing
4. No clear C++ frame? → Check JS-side type mismatches passed into the native binding
### C4. Node.js Core Contribution Rules
**Rebuild before testing:** Node.js embeds `lib/` JS into the binary via `js2c`. After any change to `src/` or `lib/`:
```bash
make -j$(nproc) # rebuild
make lint # JS, C++, MD, docs, YAML
make -j$(nproc) test # or test specific area
```
**Lint before every commit:**
```bash
make -j$(nproc)
make lint
# C++ changes only:
CLANG_FORMAT_START="$(git merge-base HEAD upstream/main)" make format-cpp
git --no-pager diff --exit-code
git add -A && git commit -s # -s is mandatory (DCO sign-off)
npx core-validate-commit --no-validate-metadata HEAD
```
**Commit message format:**
```
{area}: imperative description of change
Body explaining why and what, not how. Use terse subsystem-prefixed titles.
```
**NEVER** add `PR-URL:` or `Reviewed-By:` — those are added when the change lands. Every commit needs `git commit -s` (Signed-off-by).
---
## Section D: OAuth 2.0/2.1 with Fastify
### D1. Authorization Code + PKCE
```ts
// plugins/oauth.ts
import fp from 'fastify-plugin';
import oauth2 from '@fastify/oauth2';
export default fp(async function (fastify) {
fastify.register(oauth2, {
name: 'oauth2',
scope: ['openid', 'profile', 'email'],
credentials: {
client: {
id: process.env.CLIENT_ID!,
secret: process.env.CLIENT_SECRET!,
},
auth: {
authorizeHost: process.env.AUTH_SERVER!,
authorizePath: '/authorize',
tokenHost: process.env.AUTH_SERVER!,
tokenPath: '/token',
},
},
startRedirectPath: '/login',
callbackUri: process.env.CALLBACK_URI!,
pkce: 'S256', // RFC 7636 — always for public clients
generateStateFunction: (req) => req.session.state = crypto.randomUUID(),
checkStateFunction: (req, callback) =>
req.query.state === req.session.state
? callback()
: callback(new Error('State mismatch')),
});
});
```
**Validation checkpoint:** Confirm `callbackUri` matches a registered redirect URI at the auth server (RFC 6749 §3.1.2).
### D2. JWT Validation Middleware
```ts
import { FastifyRequest, FastifyReply } from 'fastify';
import jwt from '@fastify/jwt';
export async function verifyToken(request: FastifyRequest, reply: FastifyReply) {
try {
await request.jwtVerify();
const payload = request.user as Record<string, unknown>;
const now = Math.floor(Date.now() / 1000);
if (typeof payload.exp === 'number' && payload.exp < now)
return reply.code(401).send({ error: 'token_expired' });
if (payload.iss !== process.env.EXPECTED_ISSUER)
return reply.code(401).send({ error: 'invalid_issuer' });
if (payload.aud !== process.env.EXPECTED_AUDIENCE)
return reply.code(401).send({ error: 'invalid_audience' });
} catch (err) {
return reply.code(401).send({ error: 'invalid_token' });
}
}
```
Validate `exp`, `iss`, `aud`, and `sub` on every request (RFC 7519 §4). Use asymmetric signing (RS256/ES256 via JWKS) for third-party tokens.
### D3. Refresh Token Rotation
```ts
async function refreshAccessToken(fastify, refreshToken: string) {
const newToken = await fastify.oauth2.getNewAccessTokenUsingRefreshTokenFlow({
refresh_token: refreshToken,
});
// Replace stored refresh token on every use (RFC 6749 §10.4)
return {
accessToken: newToken.token.access_token,
refreshToken: newToken.token.refresh_token ?? refreshToken,
};
}
```
### D4. Security Checklist
- Validate redirect URI against allowlist (RFC 6749 §3.1.2)
- PKCE S256 for all public clients (RFC 7636 §4.2)
- Validate `state` to prevent CSRF (RFC 6749 §10.12)
- Validate `iss`, `aud`, `exp` on every JWT (RFC 7519 §4)
- Rotate refresh tokens on every use (RFC 6749 §10.4)
- HTTPS everywhere; reject HTTP redirect URIs (RFC 6749 §3.1.2.1)
- Rate-limit token endpoints (OAuth 2.1 §7)
### D5. Anti-Patterns to Avoid
- **Storing tokens in localStorage** — use HttpOnly, Secure, SameSite=Strict cookies
- **Skipping audience validation** — allows token reuse across services
- **Using implicit flow** — deprecated in OAuth 2.1; use authorization code + PKCE
- **Symmetric signing (HS256) for third-party tokens** — use RS256/ES256 with JWKS
---
## Section E: Advanced TypeScript Types
### E1. Eliminating `any`
**Before** — raw `any`:
```ts
function getProperty(obj: any, key: string): any { return obj[key]; }
```
**After** — generic constraint:
```ts
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// getProperty({ name: "Alice" }, "name") → inferred as string ✓
```
**Narrowing an unknown API response:**
```ts
interface User { id: number; name: string; }
function isUser(value: unknown): value is User {
return typeof value === 'object' && value !== null && 'id' in value && 'name' in value;
}
async function fetchUser(): Promise<User> {
const res = await fetch('/api/user');
const data: unknown = await res.json();
if (!isUser(data)) throw new Error('Invalid user shape');
return data;
}
```
### E2. Conditional Types & infer
```ts
// Extract the resolved type from a Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Result = Unwrap<Promise<string>>; // string
// Extract array element type
type Element<T> = T extends (infer U)[] ? U : never;
type Elem = Element<string[]>; // string
// Function return type (like ReturnType)
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
```
### E3. Template Literal Types
```ts
type EventName<T extends string> = `${T}Changed`;
type UserEvent = EventName<'user'>; // "userChanged"
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = `/api/${string}`;
type Route = `[${HttpMethod}] ${ApiPath}`;
// "[GET] /api/users"
```
### E4. Mapped Types
```ts
type Readonly<T> = { readonly [K in keyof T]: T[K]; };
type Optional<T> = { [K in keyof T]?: T[K]; };
type Nullable<T> = { [K in keyof T]: T[K] | null; };
// Deep partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
```
### E5. Branded/Opaque Types
```ts
// Type-safe identifiers — prevents mixing up UserId and OrderId
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function getUser(id: UserId): User { /* ... */ }
const userId = 'abc123' as UserId;
const orderId = 'xyz789' as OrderId;
getUser(userId); // ✓ OK
getUser(orderId); // ✗ Type error — can't pass OrderId where UserId is expected
```
### E6. Type Workflow
When facing TypeScript errors:
1. Run `tsc --noEmit` to capture full error output
2. Identify root cause (unsound inference, missing constraints, implicit `any`)
3. Craft precise, type-safe solutions
4. Eliminate all `any` types — validate each replacement satisfies call sites
5. Confirm with a second `tsc --noEmit` pass
---
## Pitfalls
1. **Type stripping only works with Node 22.6+.** Check the Node.js version before using `--experimental-strip-types`. On older versions, fall back to `tsx` or `ts-node`.
2. **`import type` is mandatory for type stripping.** Using `import` for type-only imports will try to resolve the module at runtime and fail.
3. **`enum` and `namespace` don't work with type stripping.** Use `const` objects and plain modules instead.
4. **Streams: pipelines are lazy.** Without `await pipeline(...)` the stream never runs. Always await or return the promise.
5. **Fastify encapsulation hides decorators from sibling plugins.** Use `fastify-plugin` to share functionality across plugins.
6. **Never skip the rebuild step when contributing to Node.js core.** After editing `src/` or `lib/`, changes don't take effect until `make -j$(nproc)` runs.
7. **Never commit without `-s` in Node.js core.** Every commit must be `Signed-off-by` (DCO).
8. **OAuth state parameter must be cryptographically random and session-bound.** Using a fixed string or time-based seed breaks CSRF protection.
9. **`any` types propagate silently.** One `any` in a chain disables type checking for everything it touches. Always remove `any` from the source, not just the call site.
10. **Symmetric signing (HS256) with third-party OAuth providers is a security risk.** Only use asymmetric (RS256/ES256) and validate against a JWKS endpoint.
---
## Verification
Before shipping code produced with this skill:
| Check | Section | How |
|-------|---------|-----|
| `tsc --noEmit` passes | A1, E | Run TypeScript compiler |
| `node --test` passes | A6 | Run test suite |
| JWT validation covers exp/iss/aud | D2 | Verify hook/middleware claims check |
| Graceful shutdown handles SIGTERM | A5 | Send SIGTERM and confirm cleanup |
| Stream pipeline is awaited | A3 | Check for `await pipeline(...)` |
| All `any` types removed | E1, E6 | `tsc --noEmit --strict` shows 0 errors |
| Fastify plugins use `fastify-plugin` | B2 | Inspect shared decorators |
| No OAuth implicit flow | D4 | Confirm PKCE+authorization code |
| npm audit clean | A0 | `npm audit` |