CreationalTypeScriptverifiedVerified
Builder Pattern in TypeScript
Separates the construction of a complex object from its representation, allowing the same construction process to produce different results.
How to Implement the Builder Pattern in TypeScript
1Step 1: Define the product class
class House {
walls = 0;
doors = 0;
windows = 0;
hasGarage = false;
hasPool = false;
describe(): string {
const features = [
`${this.walls} walls`, `${this.doors} doors`, `${this.windows} windows`,
this.hasGarage ? "garage" : null,
this.hasPool ? "pool" : null,
].filter(Boolean);
return `House with ${features.join(", ")}`;
}
}2Step 2: Create the Builder with a fluent API
class HouseBuilder {
private house = new House();
reset(): this { this.house = new House(); return this; }
setWalls(n: number): this { this.house.walls = n; return this; }
setDoors(n: number): this { this.house.doors = n; return this; }
setWindows(n: number): this { this.house.windows = n; return this; }
setGarage(v: boolean): this { this.house.hasGarage = v; return this; }
setPool(v: boolean): this { this.house.hasPool = v; return this; }
build(): House {
const result = this.house;
this.reset();
return result;
}
}3Step 3: Add a Director for preset configurations
class Director {
buildMinimal(b: HouseBuilder): House {
return b.reset().setWalls(4).setDoors(1).setWindows(2).build();
}
buildLuxury(b: HouseBuilder): House {
return b.reset().setWalls(8).setDoors(4).setWindows(12)
.setGarage(true).setPool(true).build();
}
}4Step 4: Build houses with and without a director
const builder = new HouseBuilder();
const director = new Director();
console.log(director.buildMinimal(builder).describe());
console.log(director.buildLuxury(builder).describe());
// Or build manually
const custom = builder.setWalls(6).setDoors(2).setWindows(8).setPool(true).build();
console.log(custom.describe());// ── HTTP Request Builder ──────────────────────────────────────────
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
interface RetryPolicy {
maxRetries: number;
baseDelayMs: number;
retryOn: number[];
}
interface RequestConfig {
readonly method: HttpMethod;
readonly url: string;
readonly headers: Readonly<Record<string, string>>;
readonly body: unknown;
readonly timeoutMs: number;
readonly retry: Readonly<RetryPolicy>;
}
class RequestBuilder {
private config = {
method: "GET" as HttpMethod,
url: "",
headers: {} as Record<string, string>,
body: undefined as unknown,
timeoutMs: 30_000,
retry: { maxRetries: 0, baseDelayMs: 1000, retryOn: [429, 500, 502, 503] },
};
static create(): RequestBuilder { return new RequestBuilder(); }
method(m: HttpMethod): this { this.config.method = m; return this; }
url(u: string): this { this.config.url = u; return this; }
header(key: string, value: string): this { this.config.headers[key] = value; return this; }
bearerToken(token: string): this { this.config.headers["Authorization"] = `Bearer ${token}`; return this; }
json(data: unknown): this {
this.config.body = data;
this.config.headers["Content-Type"] = "application/json";
return this;
}
timeout(ms: number): this {
if (ms <= 0) throw new Error("Timeout must be positive");
this.config.timeoutMs = ms;
return this;
}
retries(max: number, baseDelayMs = 1000): this {
this.config.retry = { ...this.config.retry, maxRetries: max, baseDelayMs };
return this;
}
build(): RequestConfig {
if (!this.config.url) throw new Error("URL is required");
if (this.config.body && (this.config.method === "GET" || this.config.method === "DELETE")) {
throw new Error(`${this.config.method} requests should not have a body`);
}
return Object.freeze({ ...this.config, headers: Object.freeze({ ...this.config.headers }) });
}
}
// Executor
async function executeRequest<T = unknown>(config: RequestConfig): Promise<{ status: number; data: T; attempts: number }> {
let lastError: Error | null = null;
const maxAttempts = config.retry.maxRetries + 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body ? JSON.stringify(config.body) : undefined,
});
if (!response.ok && config.retry.retryOn.includes(response.status) && attempt < maxAttempts) {
await new Promise(r => setTimeout(r, config.retry.baseDelayMs * 2 ** (attempt - 1)));
continue;
}
return { status: response.status, data: (await response.json()) as T, attempts: attempt };
} catch (e) {
lastError = e instanceof Error ? e : new Error(String(e));
if (attempt === maxAttempts) break;
}
}
throw lastError ?? new Error("Request failed");
}
export { RequestBuilder, executeRequest, type RequestConfig, type RetryPolicy };Builder Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Builder Pattern in the Real World
“Consider ordering a custom sandwich at a deli. You tell the sandwich artist (builder) each step — bread type, protein, toppings, sauce — and they assemble it in the right order. You don’t need to know how to layer ingredients properly; you just specify what you want, and the builder hands you a finished sandwich.”