npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

api-schema-mapper

v1.0.0

Published

A lightweight library for mapping between inconsistent GET/POST/PATCH API schemas and managing form state transformations

Readme

API Schema Mapper

A lightweight, zero-dependency JavaScript library for mapping between inconsistent GET/POST/PATCH API schemas and managing form state transformations.

The Problem

You're working with a REST API where:

  • GET returns data with one set of field names (user_name, email_address)
  • POST/PATCH expects different field names (username, email)
  • You need to track only user changes and send minimal PATCH payloads
  • You're tired of writing repetitive mapping and diffing logic for every form

The Solution

API Schema Mapper provides a declarative, configuration-driven approach to:

✅ Normalize API data → form schema
✅ Denormalize form data → API payload
✅ Compute minimal diffs for PATCH requests
✅ Handle nested objects and arrays
✅ Type coercion and validation hooks

Installation

npm install api-schema-mapper

Or copy the src/ directory into your project.

Quick Start

const Mapper = require('api-schema-mapper');

// Define schema mapping (API field names → form field names)
const mapper = new Mapper({
  apiToForm: {
    user_name: 'username',
    contact: {
      email_address: 'email',
      phone_number: 'phone'
    }
  }
});

// GET /user returns API data
const apiResponse = {
  user_name: 'john_doe',
  contact: {
    email_address: '[email protected]',
    phone_number: '555-1234'
  }
};

// Normalize to form schema
const formData = mapper.normalize(apiResponse);
// { username: 'john_doe', email: '[email protected]', phone: '555-1234' }

// User edits form
const editedForm = {
  ...formData,
  email: '[email protected]'
};

// Build minimal PATCH payload
const patchPayload = mapper.buildPatch(formData, editedForm);
// { contact: { email_address: '[email protected]' } }

// Send to API
await fetch('/user', {
  method: 'PATCH',
  body: JSON.stringify(patchPayload)
});

Core Concepts

1. Schema Mapping

Define how API fields map to form fields:

const mapping = {
  apiToForm: {
    api_field_name: 'formFieldName',
    nested_object: {
      api_nested_field: 'formNestedField'
    }
  }
};

2. Normalization

Transform API data → form schema:

const formData = mapper.normalize(apiResponse);

3. Denormalization

Transform form data → API payload:

const apiPayload = mapper.denormalize(formData);

4. Diffing

Compute minimal changes:

const patchPayload = mapper.buildPatch(initialForm, currentForm);

API Reference

new Mapper(config)

Create a new mapper instance.

Parameters:

{
  apiToForm: Object,      // Required: API to form field mapping
  formToApi: Object,      // Optional: Explicit form to API mapping (auto-inverted)
  transforms: Object,     // Optional: Custom transformation functions
  defaults: Object,       // Optional: Default form values
  validator: Function,    // Optional: Validation function
  options: {
    typeCoercion: boolean,    // Auto-convert types (default: true)
    omitUndefined: boolean,   // Omit undefined in payloads (default: true)
    omitNull: boolean,        // Omit null in payloads (default: false)
    compareArrays: boolean    // Deep array comparison (default: true)
  }
}

Example:

const mapper = new Mapper({
  apiToForm: {
    user_name: 'username',
    email_address: 'email'
  },
  defaults: {
    role: 'user'
  },
  options: {
    typeCoercion: true
  }
});

Instance Methods

normalize(apiData)

Transform API data to form schema.

const formData = mapper.normalize(apiResponse);

denormalize(formData)

Transform form data to API payload.

const apiPayload = mapper.denormalize(formData);

buildPatch(initialForm, currentForm, options?)

Build PATCH payload with only changed fields.

const patchPayload = mapper.buildPatch(initial, current);
// Returns null if no changes

buildPost(formData, options?)

Build POST payload with all fields.

const postPayload = mapper.buildPost(formData);

buildPut(formData, options?)

Build PUT payload (complete replacement).

const putPayload = mapper.buildPut(formData);

diff(original, current)

Compute differences between two objects.

const changes = mapper.diff(initial, current);
// Returns object with only changed fields

hasChanges(original, current)

Check if objects have differences.

if (mapper.hasChanges(initial, current)) {
  // Has changes
}

getChangedPaths(original, current)

Get array of changed field paths.

const paths = mapper.getChangedPaths(initial, current);
// ['email', 'profile.phone']

createPatchFromApi(apiData, editedForm)

Complete workflow: normalize API data and build PATCH.

const patchPayload = mapper.createPatchFromApi(apiResponse, editedForm);

Advanced Usage

Nested Object Mapping

const mapper = new Mapper({
  apiToForm: {
    user_id: 'id',
    profile: {
      full_name: 'name',
      email_address: 'email'
    },
    address: {
      street_address: 'street',
      city_name: 'city',
      postal_code: 'zipCode'
    }
  }
});

const apiData = {
  user_id: 123,
  profile: {
    full_name: 'John Doe',
    email_address: '[email protected]'
  },
  address: {
    street_address: '123 Main St',
    city_name: 'Springfield',
    postal_code: '12345'
  }
};

const formData = mapper.normalize(apiData);
/*
{
  id: 123,
  name: 'John Doe',
  email: '[email protected]',
  street: '123 Main St',
  city: 'Springfield',
  zipCode: '12345'
}
*/

Custom Transformations

const mapper = new Mapper({
  apiToForm: {
    created_at: 'createdAt',
    price_cents: 'price'
  },
  transforms: {
    createdAt: (value) => new Date(value),
    price: (value) => value / 100  // cents to dollars
  }
});

Default Values

const mapper = new Mapper({
  apiToForm: {
    user_name: 'username',
    role: 'role'
  },
  defaults: {
    role: 'user',
    status: 'active'
  }
});

const formData = mapper.normalize({ user_name: 'john' });
// { username: 'john', role: 'user', status: 'active' }

Validation

const mapper = new Mapper({
  apiToForm: {
    email_address: 'email'
  },
  validator: (data) => {
    const errors = [];
    if (!data.email || !data.email.includes('@')) {
      errors.push('Invalid email');
    }
    return {
      valid: errors.length === 0,
      errors
    };
  }
});

try {
  const payload = mapper.buildPost({ email: 'invalid' });
} catch (error) {
  console.error(error.message); // Validation failed: Invalid email
}

Standalone Functions

For advanced use cases, import functions directly:

const {
  normalize,
  denormalize,
  diff,
  buildPatchPayload,
  buildPostPayload,
  utils
} = require('api-schema-mapper');

// Use without creating mapper instance
const formData = normalize(apiData, mapping);
const changes = diff(initial, current);

Utility Functions

const { utils } = require('api-schema-mapper');

// Flatten nested objects
const flat = utils.flattenObject({ a: { b: { c: 1 } } });
// { 'a.b.c': 1 }

// Unflatten objects
const nested = utils.unflattenObject({ 'a.b.c': 1 });
// { a: { b: { c: 1 } } }

// Invert mapping
const inverted = utils.invertMapping({ api_field: 'formField' });
// { formField: 'api_field' }

// Deep clone
const cloned = utils.deepClone(object);

// Get/set nested values
const value = utils.getNestedValue(obj, 'user.profile.email');
utils.setNestedValue(obj, 'user.profile.email', '[email protected]');

Real-World Example

// User profile form with complex API schema
const profileMapper = new Mapper({
  apiToForm: {
    user_id: 'id',
    user_name: 'username',
    personal_info: {
      first_name: 'firstName',
      last_name: 'lastName',
      date_of_birth: 'birthDate'
    },
    contact_details: {
      primary_email: 'email',
      phone_number: 'phone'
    },
    preferences: {
      receive_notifications: 'notifications',
      theme_setting: 'theme'
    }
  },
  defaults: {
    notifications: true,
    theme: 'light'
  },
  transforms: {
    birthDate: (value) => value ? new Date(value) : null
  }
});

// React component usage
function UserProfileForm() {
  const [initialForm, setInitialForm] = useState(null);
  const [currentForm, setCurrentForm] = useState(null);

  useEffect(() => {
    // Fetch user data
    fetch('/api/user/123')
      .then(res => res.json())
      .then(apiData => {
        const normalized = profileMapper.normalize(apiData);
        setInitialForm(normalized);
        setCurrentForm(normalized);
      });
  }, []);

  const handleSave = async () => {
    // Build minimal PATCH payload
    const payload = profileMapper.buildPatch(initialForm, currentForm);
    
    if (!payload) {
      alert('No changes to save');
      return;
    }

    await fetch('/api/user/123', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
    
    // Update initial form after successful save
    setInitialForm(currentForm);
  };

  return (
    <form>
      <input
        value={currentForm?.username || ''}
        onChange={(e) => setCurrentForm({
          ...currentForm,
          username: e.target.value
        })}
      />
      {/* More fields... */}
      <button onClick={handleSave}>Save Changes</button>
    </form>
  );
}

Running Tests

npm test

Running Examples

node examples/basic-usage.js

Features

  • ✅ Zero dependencies
  • ✅ Lightweight (~3KB minified)
  • ✅ Full TypeScript support (types included)
  • ✅ Works in Node.js and browsers
  • ✅ Comprehensive test coverage
  • ✅ Production-ready

Use Cases

  • Form data synchronization with REST APIs
  • Redux/MobX state management with API integration
  • GraphQL to REST adapter layer
  • Multi-step form wizards
  • Optimistic UI updates
  • Undo/redo functionality
  • Change tracking and audit logs

Performance

The library is designed for efficiency:

  • Lazy evaluation - only processes changed fields
  • Minimal memory allocation
  • No external dependencies
  • Optimized for common CRUD operations

Benchmarks on typical form data (20 fields):

  • Normalize: ~0.1ms
  • Denormalize: ~0.1ms
  • Diff: ~0.2ms
  • Build PATCH: ~0.3ms

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)
  • Node.js 12+

Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

ISC

Author

Built with ❤️ for developers dealing with inconsistent APIs.

Related Projects

FAQ

Q: Can I use this with TypeScript?
A: Yes! Type definitions are included in the package.

Q: Does it work with arrays?
A: Yes, arrays are fully supported including deep comparison.

Q: Can I transform values during mapping?
A: Yes, use the transforms option in the config.

Q: How do I handle API versioning?
A: Create separate Mapper instances for each API version.

Q: Does it support nested arrays of objects?
A: Yes, nested arrays are fully supported.

Q: Can I use it with GraphQL?
A: Yes, though it's primarily designed for REST APIs.

Support


Star this repo if you find it useful!