Designing Type-Safe API Boundaries
This is an example post for testing Markdown rendering, not a production API client. The snippets deliberately mix short lines with lines long enough to require horizontal scrolling or wrapping.
A useful API boundary translates an unreliable network into a small set of predictable outcomes. That sounds simple until you need cancellation, validation, helpful error messages, and a UI that distinguishes an empty result from a failed request.
Start with the result, not the request
The type Result<ReadonlyArray<DeploymentSummary>, DeploymentRequestError> should sit comfortably inside a paragraph. A much longer identifier, organizationSettings.experimentalDeploymentObservability.maximumPermittedConcurrentBackgroundRefreshRequests, tests what happens when inline code has no convenient spaces.
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E }
type DeploymentSummary = {
id: string
service: string
environment: "preview" | "production"
status: "queued" | "running" | "complete"
}
type DeploymentRequestError = {
kind: "http" | "network" | "invalid-response"
message: string
}
function isDeployment(value: unknown): value is DeploymentSummary {
if (typeof value !== "object" || value === null) return false
const item = value as Record<string, unknown>
return typeof item.id === "string" && typeof item.service === "string" && (item.environment === "preview" || item.environment === "production") && (item.status === "queued" || item.status === "running" || item.status === "complete")
}
export async function getDeployments(organizationId: string, signal?: AbortSignal): Promise<Result<ReadonlyArray<DeploymentSummary>, DeploymentRequestError>> {
const url = new URL(`/api/organizations/${encodeURIComponent(organizationId)}/deployments`, "https://api.example.com")
url.searchParams.set("include", "service,environment,status,createdAt,completedAt,triggeredBy,commitSha,healthChecks")
try {
const response = await fetch(url, {
signal,
headers: { Accept: "application/json" },
})
if (!response.ok) {
return { ok: false, error: { kind: "http", message: `Deployment lookup failed with HTTP ${response.status} for organization ${organizationId}; inspect the request identifier before retrying this operation.` } }
}
const payload: unknown = await response.json()
if (!Array.isArray(payload) || !payload.every(isDeployment)) {
return { ok: false, error: { kind: "invalid-response", message: "Expected an array of deployment summaries" } }
}
return { ok: true, value: payload }
} catch (error) {
if (signal?.aborted) throw error
return { ok: false, error: { kind: "network", message: error instanceof Error ? error.message : "An unknown network error occurred" } }
}
}
Notice the difference between a valid empty array and a response we cannot understand. Collapsing both into [] makes the interface look reassuring precisely when it should be asking for attention.
Render each state explicitly
This standalone TSX fixture uses a discriminated union rather than several booleans that could contradict each other.
import { useId } from "react"
type Deployment = {
id: string
service: string
status: "queued" | "running" | "complete"
}
type Props = {
state:
| { kind: "loading" }
| { kind: "error"; message: string }
| { kind: "ready"; deployments: Deployment[] }
onRetry: () => void
}
export function DeploymentPanel({ state, onRetry }: Props) {
const headingId = useId()
if (state.kind === "loading") {
return <p role="status">Loading the most recent deployments…</p>
}
if (state.kind === "error") {
return (
<section aria-labelledby={headingId}>
<h2 id={headingId}>We couldn’t load your deployments</h2>
<p role="alert">{state.message}</p>
<button type="button" onClick={onRetry}>Try again</button>
</section>
)
}
return (
<section aria-labelledby={headingId} data-observability-component="organization-deployment-history-with-explicit-loading-error-and-empty-states">
<h2 id={headingId}>Recent deployments</h2>
{state.deployments.length === 0 ? (
<p>No deployments yet. Your first successful build will appear here.</p>
) : (
<ul>
{state.deployments.map((deployment) => (
<li key={deployment.id} className="deployment-history-item deployment-history-item--with-service-name-and-human-readable-status">
<a href={`/deployments/${encodeURIComponent(deployment.id)}`}>{deployment.service}</a>
<span aria-label={`Deployment status: ${deployment.status}`}>{deployment.status}</span>
</li>
))}
</ul>
)}
</section>
)
}
A review checklist with nested content
-
Validate external data.
- Check the container before its members.
- Check discriminators as well as primitive types.
- A string is not necessarily an allowed status.
- A number is not necessarily a useful timeout.
-
Preserve useful failures.
A second paragraph inside a list item should align with its text, not with the bullet. It should also have enough spacing to remain distinct from the next item.
A fallback is a product decision, not just a convenient way to satisfy the compiler.
-
Keep the rendering contract small.
- Loading means the first result is not available.
- Empty means the request succeeded with no records.
- Error means we cannot claim either of those things.
What should stay out of this helper?
Authentication refresh, pagination, and retries all deserve explicit policies. This example intentionally omits them. A helper that silently retries a mutation can be much more dangerous than a helper that merely contains a few duplicated lines.
Layout checks: long title in the index and article header; tall and wide code blocks; inline code wrapping; nested list indentation; spacing before and after a blockquote inside a list.