Back to TypeScript tutorials
Advanced14 min read

TypeScript with Node.js

Type Node.js services with correct module settings, environment variables, and framework patterns.

Node ESM and TypeScript Configuration

Node ESM projects use `"module": "NodeNext"` and `"moduleResolution": "NodeNext"` with `"type": "module"` in package.json. Import paths may require `.js` extensions in source TS files mapping to emitted files — follow Node ESM rules.

Dual CJS/ESM packages are complex — new services should pick ESM unless integrating with CJS-only dependencies. Use `tsx` or `ts-node` with ESM loader for development.

  • NodeNext module settings for ESM
  • Extension requirements in imports
  • ESM-first for new services
import { createServer } from './server.js'; // .js in TS source for Node ESM

Typing Environment Variables

Validate env vars at startup with Zod or envalid — infer TypeScript types from schemas rather than casting `process.env`. Centralize env access in one module that throws on missing required vars.

Augment Node types for known env keys via declaration merging on `ProcessEnv` only when validation module is guaranteed to run first — schema validation is safer than blind augmentation.

  • Validate env at startup with schema
  • Centralize process.env access
  • Infer types from validation schema
const envSchema = z.object({ DATABASE_URL: z.string().url() });
export const env = envSchema.parse(process.env);

Express, Fastify, and Route Handlers

Express handlers type Request, Response, and NextFunction — use generics on Request for typed params and body: `Request<{ id: string }, {}, CreateBody>`. Fastify schema declarations generate types automatically.

Next.js Route Handlers export typed functions `(request: NextRequest) => NextResponse`. Share Zod schemas between client forms and server validation for end-to-end type alignment.

  • Generic Request types in Express
  • Fastify schema-driven types
  • Shared Zod schemas client/server

Database and ORM Typing

Prisma generates types from schema — regenerate after migrations. Drizzle infers types from table definitions. Raw SQL queries return unknown rows unless typed with explicit interfaces or query builders.

Avoid `any` on database rows — define DTO types at repository boundaries and map ORM entities to domain types when layers diverge.

  • Regenerate ORM types after schema changes
  • DTO types at repository boundaries
  • Type raw SQL results explicitly

Testing and Mocking in Node

Use `vitest` or `jest` with `ts-jest`/`esbuild-jest`. Mock modules with typed `vi.mocked()` or `jest.mocked()`. For integration tests, type supertest responses or use contract tests against OpenAPI specs.

`@types/node` versions should match runtime Node major version. Enable `types: ["node"]` explicitly in tsconfig when DOM types pollute server-only projects.

  • Typed mocks with mocked helpers
  • Match @types/node to runtime version
  • Exclude DOM types in server tsconfig

Get In Touch


Ready to discuss your next project? Drop me a message.