Facade Pattern
Provide a single simplified interface to a complex subsystem.
The Problemβ
You have a complex user registration flow: validate input, hash password, save to database, send welcome email. The naive approach scatters all these calls at every registration site.
// Scattered across controllers, routes, tests...
const validated = validateUser(data);
const hashed = await hashPassword(validated.password);
const user = await saveToDb({ ...validated, password: hashed });
await sendWelcomeEmail(user.email);
Every place that registers a user repeats the same sequence. Change the order or add a step? Hunt down every call site.
The Solutionβ
A function that orchestrates the subsystems. That's it.
const validateUser = (data: UserInput) => { /* ... */ };
const hashPassword = (password: string) => { /* ... */ };
const saveToDb = (user: User) => { /* ... */ };
const sendWelcomeEmail = (email: string) => { /* ... */ };
// Facade: one function, one place
async function registerUser(data: UserInput): Promise<User> {
const validated = validateUser(data);
const hashed = await hashPassword(validated.password);
const user = await saveToDb({ ...validated, password: hashed });
await sendWelcomeEmail(user.email);
return user;
}
// Client only sees registerUser
await registerUser({ name: "Alice", email: "alice@example.com", password: "..." });
No class needed. A function that calls functions is already a facade.
This solution doesn't use Pithos. That's the point.
In functional TypeScript, every function that simplifies a complex operation is a facade. Eidos exports a @deprecated createFacade() function that exists only to guide you here.
Live Demoβ
Type a user ID and click Fetch. Toggle between "Without Facade" (6 steps executing visually one by one) and "With Facade" (one fetchUser(id) call). Same output, radically different experience.
- facade.ts
- Usage
/**
* API Request Facade.
*
* 6 subsystem steps vs 1 facade function β same result, different experience.
* fetchUser() is the facade: one call orchestrates everything.
*/
import type { User } from "./types";
const USERS: Record<number, User> = {
1: { id: 1, name: "Alice Martin", email: "alice@example.com", role: "admin", lastLogin: "2026-03-25T14:30:00Z" },
2: { id: 2, name: "Bob Chen", email: "bob@example.com", role: "editor", lastLogin: "2026-03-24T09:15:00Z" },
3: { id: 3, name: "Clara Dupont", email: "clara@example.com", role: "viewer", lastLogin: "2026-03-20T18:45:00Z" },
42: { id: 42, name: "Douglas Adams", email: "douglas@example.com", role: "admin", lastLogin: "1979-10-12T00:00:00Z" },
};
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
export async function validateInput(userId: number): Promise<string> {
await delay(300);
if (!Number.isInteger(userId) || userId < 1) throw new Error(`Invalid user ID: ${userId}`);
return `ID ${userId} is valid`;
}
export async function buildAuthHeaders(): Promise<string> {
await delay(400);
return `Authorization: Bearer ${btoa(`demo-token-${Date.now()}`).slice(0, 20)}β¦`;
}
export async function serializeRequest(userId: number): Promise<string> {
await delay(200);
return `POST /api/users β ${JSON.stringify({ id: userId, fields: ["name", "email", "role", "lastLogin"] }).slice(0, 40)}β¦`;
}
export async function executeFetch(userId: number): Promise<User> {
await delay(500);
const user = USERS[userId];
if (!user) throw new Error(`User ${userId} not found`);
return user;
}
export async function parseResponse(user: User): Promise<string> {
await delay(250);
if (!user.name || !user.email) throw new Error("Malformed response");
return `Parsed: ${user.name} <${user.email}>`;
}
export async function formatResult(user: User): Promise<User> {
await delay(150);
return { ...user, lastLogin: new Date(user.lastLogin).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }) };
}
/** The Facade β one call, same result */
export async function fetchUser(userId: number): Promise<User> {
await validateInput(userId);
await buildAuthHeaders();
await serializeRequest(userId);
const raw = await executeFetch(userId);
await parseResponse(raw);
return formatResult(raw);
}
import { useState, useCallback } from "react";
import { validateInput, buildAuthHeaders, serializeRequest, executeFetch, parseResponse, formatResult, fetchUser } from "@/lib/facade";
import { STEP_KEYS, makeInitialSteps } from "@/data/steps";
import type { User, StepState, Mode } from "@/lib/types";
export function useApiFacade() {
const [mode, setMode] = useState<Mode>("expanded");
const [userId, setUserId] = useState("42");
const [steps, setSteps] = useState(makeInitialSteps);
const [result, setResult] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const [facadeDuration, setFacadeDuration] = useState<number | null>(null);
const [totalExpandedDuration, setTotalExpandedDuration] = useState<number | null>(null);
const reset = useCallback(() => {
setSteps(makeInitialSteps());
setResult(null);
setError(null);
setFacadeDuration(null);
setTotalExpandedDuration(null);
}, []);
const updateStep = (key: string, update: Partial<StepState>) => {
setSteps((prev) => ({ ...prev, [key]: { ...prev[key], ...update } }));
};
const runStep = async (key: string, fn: () => Promise<string>) => {
updateStep(key, { status: "running" });
const t0 = performance.now();
const detail = await fn();
updateStep(key, { status: "done", detail, durationMs: performance.now() - t0 });
};
const runExpanded = useCallback(async () => {
reset();
setRunning(true);
const id = Number(userId);
const t0 = performance.now();
try {
await runStep("validate", () => validateInput(id));
await runStep("auth", () => buildAuthHeaders());
await runStep("serialize", () => serializeRequest(id));
let raw: User | undefined;
await runStep("fetch", async () => { raw = await executeFetch(id); return `Found: ${raw.name}`; });
const fetched = raw as User;
await runStep("parse", () => parseResponse(fetched));
let formatted: User | undefined;
await runStep("format", async () => { formatted = await formatResult(fetched); return "Dates & fields formatted"; });
setTotalExpandedDuration(performance.now() - t0);
setResult(formatted as User);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
setError(msg);
setSteps((prev) => {
const updated = { ...prev };
for (const key of STEP_KEYS) {
if (updated[key].status === "running") updated[key] = { ...updated[key], status: "error", detail: msg };
}
return updated;
});
} finally { setRunning(false); }
}, [userId, reset]);
const runFacade = useCallback(async () => {
reset();
setRunning(true);
const id = Number(userId);
try {
const t0 = performance.now();
const user = await fetchUser(id);
setFacadeDuration(performance.now() - t0);
setResult(user);
} catch (e) { setError(e instanceof Error ? e.message : String(e)); }
finally { setRunning(false); }
}, [userId, reset]);
const doneCount = STEP_KEYS.filter((k) => steps[k].status === "done").length;
return {
mode, setMode, userId, setUserId, steps, result, error, running,
facadeDuration, totalExpandedDuration, doneCount,
reset, handleFetch: mode === "expanded" ? runExpanded : runFacade,
};
}
APIβ
- facade
@deprecatedβ just write a function
Related
- Eidos: Design Patterns Module All 23 GoF patterns reimagined for functional TypeScript
- Why FP over OOP? The philosophy behind Eidos: no classes, no inheritance, just functions and types
- Zygos Result Combine your facades with
Resultfor typed error handling