This episode covers TypeScript on the server side: running TypeScript files with tsx and Node.js type stripping, @types/node types, typing process.env with validation, and safe async and error handling patterns.

The backend is where the type system works hardest. There you find data from databases, requests from clients, and configuration from the environment. Without types, a small mistake in any of these can become production downtime. TypeScript in Node.js changes all of that.
Node.js doesn't run TypeScript directly, so an execution path is needed: a transpiler like tsx, or Node.js's built-in type stripping in newer versions. Once running, you get full types for the Node API and guarantees over values coming from the outside.
Episode 16 covers running TypeScript in Node.js, installing @types/node, typing process.env safely, and writing async code that is robust against errors.
tsx is the simplest runner for TypeScript:
bun add -d tsx
bunx tsx src/server.tsThe command bunx tsx src/server.ts runs a TypeScript file directly without a build step. tsx loads modules, handles path aliases, and suits development with watch:
bunx tsx watch src/server.tsWatch mode monitors files and restarts the server on changes. For production, you still compile to JavaScript with tsc and run the output, as will be covered in episode 20.
Node.js version 22 and up can run TypeScript without a transpiler:
node --experimental-strip-types src/server.tsThe --experimental-strip-types flag makes Node strip type syntax before executing. Because it only removes types without transformation, runtime features like enums and namespaces need special handling. Tools like tsx remain the most flexible for various projects.
With @types/node, the whole Node.js API is typed:
bun add -d @types/nodeimport { readFile } from "node:fs/promises";
async function bacaConfig(path: string): Promise<string> {
return readFile(path, "utf-8");
}The @types/node package provides types for built-in modules like fs, path, and http. Imports with the node: prefix clarify where the module comes from and are fully supported by the declarations. Methods like readFile already have return types you can rely on.
process.env is loosely typed as string | undefined by default. Secure it with a validation layer:
function envWajib(nama: string): string {
const nilai = process.env[nama];
if (!nilai) {
throw new Error(`Variabel lingkungan ${nama} tidak diatur`);
}
return nilai;
}
const apiUrl = envWajib("API_URL");
const port = Number(envWajib("PORT"));The envWajib function forces a variable to exist or throws at startup. An error is better to happen early than for the app to run with wrong configuration. Converting with Number also gives a clear number type for numeric values.
Tip
Don't call envWajib inside a function that runs repeatedly. Call it once at module level and store the result in a constant. Configuration failures will be detected the first time the process loads.
Backend code is full of async. Safe patterns must handle rejected promises:
type Hasil = { kode: number; data?: unknown };
async function panggilApi(): Promise<Hasil> {
try {
const respons = await fetch("https://api.contoh.id/data");
if (!respons.ok) {
throw new Error(`HTTP ${respons.status}`);
}
const data: unknown = await respons.json();
return { kode: respons.status, data };
} catch (err) {
console.error("Gagal memanggil API:", err);
return { kode: 500 };
}
}The panggilApi function returns a uniform result shape, success or failure. Values from the outside are declared unknown and only used after validation. With the return type Promise<Hasil>, callers know exactly what shape they'll receive and can process it without surprises.
Episode 16 moves you to the server side: running TypeScript with tsx or Node.js type stripping, installing @types/node, securing process.env, and writing async code that handles errors explicitly.
Key takeaways:
tsx runs TypeScript without a build; watch mode reloads automatically.--experimental-strip-types.@types/node types the entire built-in Node.js API.process.env needs validation before use.unknown return forces validation of external data before use.In the next episode 17 we'll discuss code generation, API types, and contract-driven development — deriving types from schemas so the frontend and backend always stay in sync.