As a front-end developer, your job is to deliver data to your back-end services. Delivering data often includes having forms, and forms require so much attention that I decided to talk about them.
In this article I’m gonna discuss forms and how to avoid some common mistakes I’ve experienced over the years.
0. Contract
Every single form that you create, you’re gonna have to send to your back-end services. So without a good contract, you might have to write an awful amount of validation and/or transformation on your data, among other commitments. Make sure you’re having conversations about the data, edge cases, and sometimes you could even discuss field types. We all know you should sanitize your inputs and take care of localization on inputs like dates, but I trust you’ll take care of those yourself. So before you start anything, talk it out and have a strong contract so you don’t have to predict your back-end service’s behavior.
1. Data and Payload
There are many ways you can handle your form’s data, but you usually have two ways to do it:
-
Use your own data as the source of truth.
-
Use your back-end data as the source of truth.
I’ve had cases where the business requirement forced us to do things we shouldn’t have done, but I’m talking about the normal ones.
When you make a form, you need to eventually send it to your back-end services, and you’ll most likely receive the default values from another service from your back end. If we want to use our own data, we’re gonna have to transform our data. First when the data comes in API > Form default values
and then when sending it Form values > API payload
.
If you use your own data, you can rest easy knowing that you control everything, but there’s a cost and it increases as the form grows larger. See, you need to transform the data from and to your API. These transformations often cost a lot and, if you’re good, you’ll write some helper functions to deal with it, but at the end of the day you have to define somewhere in your code all the transformations and ugly key: value
pairs. It’s a pain to work with, and you’ll need extra tooling to make the DX pleasant.
But sometimes the payload is far from your own data types, and of course you’ll have to do transformations — just try your best to write pure helper functions so you don’t give yourself too many headaches.
2. Defaults and Initialization
Defaults are not “nice to have”; they’re “Must have”s. They come from somewhere: previous submissions, user profile, locale, or all of the above. If you sprinkle defaults across components, you’ll spend days asking “why is this prefilled like that?”.
What to do instead:
Have one place (one module, one function) that assembles default values. It should know how to merge server data, local defaults, and business rules without much hassle.
Compute defaults once when the form initializes. After that, register what the user changed, not “what the defaults were supposed to be” every few seconds on every change.
Always be able to explain the origin of a default: “this came from the customer record”, “this came from a test I’m running” and so on.
Plan for async defaults (when you GET something). If the data arrives later than you anticipated, the form should gracefully take it the defaults instead of flickering, blinking and resetting fields.
3. Nested Objects and Lists (I hate this part)
Real-world payloads are nested. Addresses, contacts, line items and so on. The trick is deciding how much nesting to keep in the UI state and how to maintain your value’s identity inside lists.
For your own good, keep the shape understandable:
Two levels of nesting is fine. Three is already pushing it too far for our brain. Flatten just enough so you can render and think clearly, but try not to rip out structure the server actually needs (or do, I have, It’s not as bad as they say).
Keep IDs for list items. If you rely on positions index 0, index 1
, the second you add or remove something, your validation and error messages begin to walk across your form unpredictably.
For “sometimes this object changes shape” give it a type/kind field and treat each kind separately. That makes rendering and validation predictable instead of “if this, then that, maybe.”
Be specific about absence of value. Decide once what null
means vs ""
. If null
means “unknown” and empty string means “cleared”, keep that rule everywhere, including error handling. But most of the time we’re working on projects with predefined rules no matter right or wrong.
4. Validation and Errors
You can validate the life out of a form and still ship something that users hate. The point of validation isn’t to yell at people or make it so tight and hard that the user screems; it’s to guide them to a valid payload and be generous too, sometimes you don’t actually need too much validation, just enough so the angry PM is satisfied.
For validation, think in layers:
-
UI constraints: fast stuff like required fields, simple formats that you already has a helper function for. Keep it simple and fix things automatically when you can (trim spaces, remove separators, normalize digits).
-
Business rules: the brain-killer logic. cross-field dependencies, “if A then B is required.” Put this logic in one place and treat it like functions, avoid checkbox spaghetti as much as you can.
-
Server payload: even if your UI passes everything, the server can (and probably will) reject data for different reasons: permissions, references, state changes since load (the default values we talked about). Handle these without throwing the user back to a blank form, If you have too much inputs and you reset all of them to their blank value, most of the time, the user won’t tolerate and leave.
Conclusion (AI)
Keep the contract real, keep the defaults centralized, keep nesting under control, and treat validation like a layered system. You don’t have to make forms “fancy”; you have to make them predictable. Pick who owns the shape (you or the back end), write down the rules once, and make the rest boring. If you do that, you’ll ship faster, get fewer complaints, and your team won’t spend Fridays untangling mystery bugs in “that one form.”