Data Transformation
Converting data between form format and API format.
Overviewβ
Our credit application form uses JavaScript types like Date objects, but APIs typically work with strings. We need transformation functions:
- Deserialize: API β Form (when loading)
- Serialize: Form β API (when saving)
Date Transformersβ
The form uses Date objects for personalData.birthDate and passportData.issueDate:
src/forms/credit-application/utils/transformers.ts
// API β Form: ISO string to Date
export function deserializeDate(isoString: string | null): Date | null {
if (!isoString) return null;
return new Date(isoString);
}
// Form β API: Date to ISO string
export function serializeDate(date: Date | null): string | null {
if (!date) return null;
return date.toISOString().split('T')[0]; // "2024-01-15"
}
Complete Transformer Moduleβ
src/forms/credit-application/utils/formTransformers.ts
import type { CreditApplicationForm } from '../types/credit-application.types';
// API response type (what comes from server)
interface ApiApplicationData {
loanType: string;
loanAmount: number;
loanTerm: number;
loanPurpose: string;
personalData: {
lastName: string;
firstName: string;
middleName: string;
birthDate: string; // ISO string from API
birthPlace: string;
gender: string;
};
passportData: {
series: string;
number: string;
issueDate: string; // ISO string from API
issuedBy: string;
departmentCode: string;
};
email: string;
phoneMain: string;
// ... other fields
}
// Deserialize: API β Form
export function deserializeApplication(
api: ApiApplicationData
): Partial<CreditApplicationForm> {
return {
loanType: api.loanType as CreditApplicationForm['loanType'],
loanAmount: api.loanAmount,
loanTerm: api.loanTerm,
loanPurpose: api.loanPurpose,
personalData: {
...api.personalData,
birthDate: new Date(api.personalData.birthDate),
},
passportData: {
...api.passportData,
issueDate: new Date(api.passportData.issueDate),
},
email: api.email,
phoneMain: api.phoneMain,
};
}
// Serialize: Form β API
export function serializeApplication(
form: CreditApplicationForm
): ApiApplicationData {
return {
loanType: form.loanType,
loanAmount: form.loanAmount,
loanTerm: form.loanTerm,
loanPurpose: form.loanPurpose,
personalData: {
...form.personalData,
birthDate: form.personalData.birthDate?.toISOString().split('T')[0] ?? '',
},
passportData: {
...form.passportData,
issueDate: form.passportData.issueDate?.toISOString().split('T')[0] ?? '',
},
email: form.email,
phoneMain: form.phoneMain,
};
}
Using Transformersβ
When Loading Dataβ
src/forms/credit-application/services/api.ts
import { deserializeApplication } from '../utils/formTransformers';
export async function fetchApplication(id: string) {
const response = await fetch(`/api/applications/${id}`);
const apiData = await response.json();
// Transform API data to form format
return deserializeApplication(apiData);
}
When Saving Dataβ
src/forms/credit-application/CreditApplicationForm.tsx
import { serializeApplication } from './utils/formTransformers';
const handleSubmit = async () => {
const formData = form.getValue();
// Transform form data to API format
const apiData = serializeApplication(formData);
await fetch('/api/applications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(apiData),
});
};
Using in Componentβ
src/forms/credit-application/CreditApplicationForm.tsx
import { useMemo, useEffect, useState } from 'react';
import { createCreditApplicationForm } from './createCreditApplicationForm';
import { deserializeApplication, serializeApplication } from './utils/formTransformers';
interface Props {
applicationId?: string;
}
export function ApplicationForm({ applicationId }: Props) {
const form = useMemo(() => createCreditApplicationForm(), []);
const [isLoading, setIsLoading] = useState(!!applicationId);
// Load and deserialize
useEffect(() => {
if (!applicationId) return;
async function load() {
const response = await fetch(`/api/applications/${applicationId}`);
const apiData = await response.json();
// Deserialize: API β Form
const formData = deserializeApplication(apiData);
form.patchValue(formData);
setIsLoading(false);
}
load();
}, [form, applicationId]);
// Serialize and save
const handleSubmit = async () => {
const formData = form.getValue();
// Serialize: Form β API
const apiData = serializeApplication(formData);
await fetch('/api/applications', {
method: applicationId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(apiData),
});
};
if (isLoading) {
return <div>Loading...</div>;
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
{/* Form fields */}
<button type="submit">Save</button>
</form>
);
}
Common Transformationsβ
Phone Numberβ
// Clean phone for API (digits only)
export function serializePhone(phone: string): string {
return phone.replace(/\D/g, '');
}
// Format for display
export function deserializePhone(phone: string): string {
const digits = phone.replace(/\D/g, '');
if (digits.length === 11) {
return `+${digits[0]} (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7, 9)}-${digits.slice(9)}`;
}
return phone;
}
Passport Dataβ
// Format passport series and number
export function formatPassport(series: string, number: string): string {
return `${series} ${number}`;
}
// Parse passport string
export function parsePassport(passport: string): { series: string; number: string } {
const [series, number] = passport.split(' ');
return { series: series || '', number: number || '' };
}
Key Pointsβ
- Keep transformers pure - no side effects, just data conversion
- Handle null/undefined - always check for missing values
- Type your transformers - use TypeScript for API and form types
- Centralize transformers - keep all transformations in one place
- Test transformers - they're easy to unit test
Next Stepsβ
- Validation and Saving - Complete the flow with validation and submission