Formik forms and API integration

Using data transformers between Forms and API

Rajesh Naroth
3 min readMay 28, 2020
Image by René Beck from Pixabay

In a previous article, I detailed out how to wrangle large Formik forms using sub forms and composing them within a larger form. In typical enterprise applications, a large form’s goal is to capture user input that will be sent to a server via REST or GraphQL. The biggest challenge we face is dealing with nested API data that doesn’t bend well within a flat form. A typical use case is like this:

  • API returns a business object and hydrates the form with it.
  • User edits this and submits the values.
  • Form submit action performs an API call directly or indirectly using the edited values.

The struggle is to match Formik’s JSON values to the API’s payload and vice versa. The UX form designs may not directly align with the API. Compounding to the issue is that the API s are always not necessarily UI friendly. Some are spec-ed out for public consumption and not tailored for the user experience. This adversely affects the developer experience. Here is a strategy that can solve this gracefully:

Form values and API payload object shapes do not have to align 100%

What we needed are data transformations between these two entities.

toFormValues() and toApiPayload() are simple, pure data transformation functions. They are very easy to write unit tests for.

Now, your form is free from API data restrictions and can cater to the UX design and the component library’s form fields. For instance, data that appears in a Select pulldown doesn’t have to be arm twisted to fit the Form requirements.

Using TypeScript

TypeScript interfaces are a must when you deal with complex data shapes. Without it, the chances of human errors are too much. I highly recommend it. Essentially your data types and functions would look like this:

interface IApiValues {
user: {
id: string;
fullName: string;
}
}
interface IFormValues {
userId: string;
userName: string;
}
type toFormValuesType = (apiData: IApiValues) => IFormValues;type toApiPayloadType = (values: IFormValues) => IApiValues;

Implement your transformers:

const toFormValues: toFormValuesType = apiData => ({
userId: apiData.user.id,
userName: apiData.user.fullName
});
const toApiPayload: toApiPayloadType = values => ({
user: {
id: values.userId,
fullName: values.userName
}
});

Now plug these into your Formik form:

const submitForm = (values: IFormValues) => { 
submitToApi(toApiPayload(values));
}
<Formik
initialValues={toFormValues(apiData)}
onSubmit={submitForm}
children={YourFormContainer}
/>

The solution is quite simple. Yet, it keeps your forms clean by delegating transformation to the helper functions. They can stay true to the UX design and the available input components. You don’t have to litter your form code with data wrangling all over the place. Plus your data transformations become very unit testable.

describe("form/api data transformations", () => {  const apiData: IApiValues = {
user: {
id: "1234",
fullName: "Naroth"
}
};
const formData: IFormValues = {
userId: "1234",
userName: "Naroth"
};
it("should transform correctly", () => {
expect(toFormValues(apiData)).toEqual(formData);
expect(toApiPayload(formData)).toEqual(apiData);
});
// Alternatively you could do it in one shot
// But it is better to test them individually
it("should transform back and forth correctly", () => {
expect(toApiPayload(toFormValues(apiData))).toEqual(apiData);
});
// You can also use snapshot testing to keep a reference
// of the object. It helps to understand the logic and aids
// debugging.
it("toFormValues works", () => {
expect(toFormValues(apiData)).toMatchSnapshot();
});
});

This example applies to Formik but you can apply the same strategy with another Form handler library in a similar fashion.

--

--