Skip to content

munesoft/api-normalizer

Repository files navigation

@munesoft/api-normalizer

Universal API response normalizer. Stop writing one-off mapping code for every integration. Define a schema once, get consistent output everywhere.

npm version zero dependencies TypeScript Tests


The Problem

Every API speaks a slightly different language:

// Stripe user
{ "id": "cus_abc", "name": "Acme Corp", "email": "billing@acme.com" }

// Legacy internal API
{ "user_id": 42, "full_name": "Acme Corp", "email_address": "billing@acme.com" }

// GitHub
{ "login": "octocat", "id": 583231, "avatar_url": "..." }

Without a tool like this, you write fragile, repetitive glue code in every service, every handler, every component. A field gets renamed upstream and your app silently breaks.

@munesoft/api-normalizer solves this by letting you define a mapping schema once and normalizing any response into a predictable shape.


Quick Start

npm install @munesoft/api-normalizer
import { normalize } from "@munesoft/api-normalizer";

const raw = { user_id: 42, full_name: "Jane Doe", email_address: "jane@example.com" };

const result = normalize(raw, {
  id:    ["id", "user_id", "uuid"],
  name:  ["name", "full_name", "username"],
  email: ["email", "email_address", "user_email"],
});

// result.success === true
// result.data  === { id: 42, name: "Jane Doe", email: "jane@example.com" }

Before / After

Before — scattered glue code

// user-service.ts
const user = {
  id:    response.id ?? response.user_id ?? response.uuid,
  name:  response.name ?? response.full_name ?? response.username,
  email: response.email ?? response.email_address ?? response.user_email,
};

// order-service.ts — different shape, same pain
const order = {
  id:     payload.order_id ?? payload.id ?? payload.orderId,
  total:  Number(payload.total_amount ?? payload.amount ?? payload.price),
  status: payload.status ?? payload.order_status ?? payload.state,
};

After — one schema, zero surprises

import { normalize } from "@munesoft/api-normalizer";

const USER_SCHEMA = {
  id:    ["id", "user_id", "uuid"],
  name:  ["name", "full_name", "username"],
  email: ["email", "email_address", "user_email"],
};

const ORDER_SCHEMA = {
  id:     ["id", "order_id", "orderId"],
  total:  { keys: ["total_amount", "amount", "price"], type: "number" as const },
  status: ["status", "order_status", "state"],
};

const user  = normalize(userPayload,  USER_SCHEMA);
const order = normalize(orderPayload, ORDER_SCHEMA);

Schema Mapping Guide

Shorthand: string array

The simplest form. Candidates are tried left-to-right; the first non-empty value wins.

const schema = {
  id:   ["id", "user_id", "uuid"],
  name: ["name", "full_name"],
};

Full FieldMapping descriptor

import type { Schema } from "@munesoft/api-normalizer";

const schema: Schema = {
  id: {
    keys:      ["id", "user_id"],   // candidates, tried in order
    default:   null,                // fallback when nothing resolves
    type:      "number",            // coerce to number
    required:  true,                // throw in strict mode if missing
    transform: (v) => String(v).padStart(6, "0"), // custom transform
  },
};

Dot-notation for nested data

const schema: Schema = {
  city: { keys: ["address.city", "location.city", "city"] },
  lat:  { keys: ["address.coordinates.lat", "lat"] },
};

normalize({ address: { city: "Nairobi", coordinates: { lat: -1.286 } } }, schema);
// → { city: "Nairobi", lat: -1.286 }

API Reference

normalize(data, schema, options?)

Parameter Type Description
data unknown Raw API response — object, array, null, or undefined
schema Schema Field mapping definition
options NormalizerOptions Optional flags (see below)

Returns NormalizeResult<T>:

// Success
{ success: true; data: T; _schema?: Schema; _diff?: DiffEntry[] }

// Failure (strict mode)
{ success: false; error: string; missingFields?: string[] }

Options

Option Type Default Description
strict boolean false Throw on missing required fields
coerceTypes boolean false Enable global auto-coercion (type hints still work without this)
inferSchema boolean false Attach auto-inferred schema to result as _schema
logDiff boolean false Attach mapping diff to result as _diff
plugins Plugin[] [] Post-processing plugin pipeline

Type Coercion

Set type on any FieldMapping to coerce the resolved value:

Type Behaviour
"number" "42"42, true1
"string" 42"42"
"boolean" "true" / "1" / "yes"true
"date" Any parseable date → ISO 8601 string
"auto" Detects numeric strings, booleans, and ISO dates
const schema: Schema = {
  age:       { keys: ["age"],        type: "number"  },
  active:    { keys: ["active"],     type: "boolean" },
  createdAt: { keys: ["created_at"], type: "date"    },
};

Array Normalization

Pass an array and every element is normalized automatically:

const users = [
  { user_id: 1, full_name: "Alice" },
  { id: 2,      name: "Bob"        },
  { uuid: 3,    username: "Carol"  },
];

const result = normalize(users, USER_SCHEMA);
// result.data → [{ id:1, name:"Alice" }, { id:2, name:"Bob" }, { id:3, name:"Carol" }]

Strict Mode

Enable strict: true to catch missing required fields in production:

const result = normalize(payload, schema, { strict: true });

if (!result.success) {
  console.error(result.error);          // "Missing required fields: id, email"
  console.error(result.missingFields);  // ["id", "email"]
}

Diff Logging

See exactly what changed during normalization — great for debugging:

const result = normalize(raw, schema, { logDiff: true });

result._diff?.forEach(entry => {
  console.log(`${entry.sourceKey}${entry.outputKey}`, {
    raw:        entry.rawValue,
    normalized: entry.normalizedValue,
    coerced:    entry.coerced,
    usedDefault: entry.usedDefault,
  });
});

Plugin System

Plugins run after all field mappings and receive the fully normalized object:

import { normalize, Plugin } from "@munesoft/api-normalizer";

const auditPlugin: Plugin = {
  name: "audit",
  transform: (data, raw) => ({
    ...data,
    _normalizedAt: new Date().toISOString(),
    _source: (raw as any).__source ?? "unknown",
  }),
};

normalize(payload, schema, { plugins: [auditPlugin] });

Schema Inference

Quickly generate a starter schema from a sample response:

import { inferSchema } from "@munesoft/api-normalizer";

const sample = { user_id: 1, full_name: "Jane", email_address: "jane@x.com" };
const schema = inferSchema(sample);
// Refine the output, then use it in normalize()

TypeScript Support

Full generics support for typed output:

interface User {
  id: number;
  name: string;
  email: string;
}

const result = normalize<User>(raw, schema);

if (result.success) {
  const user = result.data; // typed as User
}

Real-World Examples

Stripe + Internal API → unified User

const CUSTOMER_SCHEMA: Schema = {
  id:        ["id"],
  name:      ["name", "company_name", "full_name"],
  email:     ["email", "email_address"],
  createdAt: { keys: ["created", "created_at", "createdAt"], type: "date" },
  plan:      { keys: ["plan", "subscription_plan"], default: "free" },
};

// Works with Stripe, your own DB, or any other source
const stripeResult   = normalize(stripeCustomer,   CUSTOMER_SCHEMA);
const internalResult = normalize(internalDbRecord, CUSTOMER_SCHEMA);
// Both produce the same shape ✓

Normalizing paginated list responses

const ORDER_SCHEMA: Schema = {
  id:     ["id", "order_id", "orderId"],
  total:  { keys: ["total", "amount", "total_price"], type: "number" },
  status: ["status", "state", "order_status"],
  items:  { keys: ["items", "line_items", "products"], default: [] },
};

const result = normalize(apiResponse.orders, ORDER_SCHEMA);

Performance

  • Zero dependencies — no bundle bloat
  • Field resolution is a simple loop over candidate keys using Object.prototype lookups — O(k) per field where k is the number of candidates
  • Array normalization is O(n × f) where n = items and f = schema fields
  • Suitable for normalizing thousands of records synchronously in a single request

Browser Support

Works in any environment that supports ES2017 (async/await, Object.entries). No window, document, or Node.js-specific APIs are used.


License

MIT © munesoft

About

Normalize inconsistent API responses with schema mapping, type coercion & field aliasing. Works with REST, Stripe, GitHub, or any JSON API.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors