Universal API response normalizer. Stop writing one-off mapping code for every integration. Define a schema once, get consistent output everywhere.
Every API speaks a slightly different language:
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.
npm install @munesoft/api-normalizerimport { 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" }// 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,
};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);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"],
};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
},
};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 }| 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[] }| 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 |
Set type on any FieldMapping to coerce the resolved value:
| Type | Behaviour |
|---|---|
"number" |
"42" → 42, true → 1 |
"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" },
};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" }]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"]
}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,
});
});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] });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()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
}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 ✓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);- Zero dependencies — no bundle bloat
- Field resolution is a simple loop over candidate keys using
Object.prototypelookups — 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
Works in any environment that supports ES2017 (async/await, Object.entries). No window, document, or Node.js-specific APIs are used.
MIT © munesoft