A tiny bidirectional DTO adapter for translating backend API contracts into frontend models and back.
Use contract-adapter when your backend speaks snake_case, your frontend speaks camelCase, and you want the conversion to live at the API boundary instead of being scattered through components, services, forms, and reducers.
Frontend and backend code often use different naming conventions.
For example, a backend API may return this:
{
"user_id": 133,
"is_logged_in": true
}But the frontend usually wants this:
{
"userId": 133,
"isLoggedIn": true
}contract-adapter gives you one explicit, reversible mapping layer:
fromServer()converts server/API data into client/frontend data.toServer()converts client/frontend data back into server/API data.- Explicit schemas are supported.
- Automatic
snakecase↔camelcaseconversion is supported. - Nested objects and arrays are supported.
- Runtime dependencies: none.
npm install contract-adapterimport ContractAdapter from 'contract-adapter';
const userFromApi = {
id: 133,
is_logged_in: true,
};
const userAdapter = new ContractAdapter('user', {
id: 'userId',
is_logged_in: 'isLoggedIn',
});
const clientUser = userAdapter.fromServer(userFromApi);
console.log(clientUser);
// {
// userId: 133,
// isLoggedIn: true
// }
const serverUser = userAdapter.toServer(clientUser);
console.log(serverUser);
// {
// id: 133,
// is_logged_in: true
// }The first constructor argument, user in this example, identifies the adapter when it is used as a nested adapter. It does not wrap the root result.
Use an object schema when you want full control over the API contract.
import ContractAdapter from 'contract-adapter';
const apiUser = {
full_name: 'John Doe',
email: 'john@example.com',
};
const userAdapter = new ContractAdapter('user', {
full_name: 'fullName',
email: 'email',
});
const frontendUser = userAdapter.fromServer(apiUser);
// {
// fullName: 'John Doe',
// email: 'john@example.com'
// }
const payload = userAdapter.toServer(frontendUser);
// {
// full_name: 'John Doe',
// email: 'john@example.com'
// }Use an array schema when you only need convention-based conversion.
The first value is the server/input style. The second value is the client/output style.
import ContractAdapter from 'contract-adapter';
const userAdapter = new ContractAdapter('user', ['snakecase', 'camelcase']);
const frontendUser = userAdapter.fromServer({
full_name: 'John Doe',
email: 'john@example.com',
});
// {
// fullName: 'John Doe',
// email: 'john@example.com'
// }
const apiPayload = userAdapter.toServer(frontendUser);
// {
// full_name: 'John Doe',
// email: 'john@example.com'
// }The currently supported style values are:
'snakecase';
'camelcase';Nested adapters let you model nested API contracts explicitly.
import ContractAdapter from 'contract-adapter';
const addressAdapter = new ContractAdapter('address', {
city: 'city',
postal_code: 'postalCode',
});
const userAdapter = new ContractAdapter('user', {
full_name: 'fullName',
address: addressAdapter,
});
const apiUser = {
full_name: 'John Doe',
address: {
city: 'New York',
postal_code: '100001',
},
};
const frontendUser = userAdapter.fromServer(apiUser);
// {
// fullName: 'John Doe',
// address: {
// city: 'New York',
// postalCode: '100001'
// }
// }
const apiPayload = userAdapter.toServer(frontendUser);
// {
// full_name: 'John Doe',
// address: {
// city: 'New York',
// postal_code: '100001'
// }
// }Automatic conversion also works with arrays of nested objects.
import ContractAdapter from 'contract-adapter';
const storeAdapter = new ContractAdapter('store', ['snakecase', 'camelcase']);
const frontendStore = storeAdapter.fromServer({
store_name: 'Super Store',
products: [
{
prod_name: 'Shampoo',
prod_price: 124,
},
{
prod_name: 'Shower Gel',
prod_price: 1234,
},
],
});
// {
// storeName: 'Super Store',
// products: [
// {
// prodName: 'Shampoo',
// prodPrice: 124
// },
// {
// prodName: 'Shower Gel',
// prodPrice: 1234
// }
// ]
// }Pass an array of keys as the second argument to unserialize() or serialize() when some keys should be preserved exactly.
import ContractAdapter from 'contract-adapter';
const adapter = new ContractAdapter('payload', ['snakecase', 'camelcase']);
const frontendPayload = adapter.fromServer(
{
full_name: 'John Doe',
raw_value: 'keep this key unchanged',
},
['raw_value'],
);
// {
// fullName: 'John Doe',
// raw_value: 'keep this key unchanged'
// }Exclusions are also passed to nested conversions.
serialize() and unserialize() are still supported.
adapter.unserialize(apiData); // same as adapter.fromServer(apiData)
adapter.serialize(clientData); // same as adapter.toServer(clientData)New code should prefer fromServer() and toServer().
Creates a new adapter.
const adapter = new ContractAdapter('user', schema);key must be a string. It is used when the adapter is embedded inside another adapter.
schema can be either an explicit mapping object or a two-item style conversion array.
Explicit mapping schema:
{
server_key: 'clientKey';
}Automatic style schema:
['snakecase', 'camelcase'][('camelcase', 'snakecase')];Converts server/API data into client/frontend data.
const clientData = adapter.fromServer(apiData);Converts client/frontend data into server/API data.
const apiPayload = adapter.toServer(clientData);Optional array of keys that should not be converted during automatic style conversion.
adapter.fromServer(data, ['raw_value']);
adapter.toServer(data, ['rawValue']);Use contract-adapter when:
- You want a clear boundary between API contracts and frontend models.
- You need conversion in both directions.
- You need explicit field mapping, not only automatic key casing.
- You have nested DTOs or arrays of DTOs.
- You want a small package with no runtime dependencies.
If you only need one-way key conversion, a general-purpose case-conversion package may be enough. contract-adapter is intended for reversible API contract adaptation.
contract-adapter is the successor to adaptr.
Install the new package:
npm uninstall adaptr
npm install contract-adapterUpdate imports:
- import Adaptr from 'adaptr';
+ import ContractAdapter from 'contract-adapter';The core API remains the same:
const adapter = new ContractAdapter('user', schema);
adapter.fromServer(apiData);
adapter.toServer(clientData);npm test: Run tests with Vitest.npm run build: Build CommonJS and ESM bundles with tsup.npm run lint: Run ESLint.npm run typecheck: Run TypeScript type check on declarations and tests.npm pack --dry-run: Verify package contents.
MIT