typescript 158 lines · 4 tabs

API error shape that frontend can rely on

Shared by codesnips Jan 2026
4 tabs
export type ErrorCode =
  | 'VALIDATION'
  | 'UNAUTHENTICATED'
  | 'FORBIDDEN'
  | 'NOT_FOUND'
  | 'CONFLICT'
  | 'RATE_LIMITED'
  | 'INTERNAL';

export interface ApiErrorBody {
  error: {
    code: ErrorCode;
    message: string;
    details?: Record<string, string[]>;
    requestId: string;
  };
}

const STATUS_BY_CODE: Record<ErrorCode, number> = {
  VALIDATION: 422,
  UNAUTHENTICATED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  CONFLICT: 409,
  RATE_LIMITED: 429,
  INTERNAL: 500,
};

export class ApiError extends Error {
  readonly code: ErrorCode;
  readonly status: number;
  readonly details?: Record<string, string[]>;

  constructor(code: ErrorCode, message: string, details?: Record<string, string[]>) {
    super(message);
    this.name = 'ApiError';
    this.code = code;
    this.status = STATUS_BY_CODE[code];
    this.details = details;
  }
}
4 files · typescript Explain with highlit

A stable error shape is one of the highest-leverage contracts in a web app: the frontend needs to distinguish a validation problem from an auth failure from an unexpected crash, and it needs to do so without string-matching human-readable messages. This snippet defines that contract once and enforces it on both ends of the wire.

In errors.ts, the shape is codified as ApiErrorBody: a discriminated envelope with a machine-readable code, a human message, an optional details map for per-field validation errors, and a requestId for correlating client reports with server logs. The ApiError class carries an HTTP status alongside the code, and the ErrorCode union is the single source of truth both sides import. Making code a closed union means the compiler flags any handler that forgets a case.

In errorMiddleware.ts, an Express error handler normalizes everything into that envelope. Known ApiError instances pass through with their status; ZodError instances become a VALIDATION response with details built from flatten().fieldErrors, so the client gets structured field messages rather than a wall of text. Anything else is treated as an unexpected INTERNAL error — the real cause is logged with the requestId but never leaked to the client. This is the key trade-off: the response is deliberately lossy so internal details stay private while remaining debuggable server-side. Note the four-argument signature, which is how Express recognizes error middleware.

In apiClient.ts, the browser side mirrors the contract. request inspects res.ok, parses the JSON envelope, and rethrows a typed ClientApiError so callers can branch on err.code instead of err.status. A network failure or non-JSON body is coerced into a synthetic INTERNAL error so callers never have to handle two failure vocabularies.

In useLogin.ts, a React hook consumes the result: on a VALIDATION code it surfaces details as per-field messages, and everything else collapses to a single banner. Because ErrorCode is shared, adding a new code forces a compile error until the UI handles it, which is exactly the safety net an evolving API wants. The pattern shines wherever multiple clients depend on predictable failures; the cost is the discipline of always throwing through the envelope.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

API error shape that frontend can rely on — share card
Link copied