Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

munesoft/api-normalizer

Open more actions menu

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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.