You build a contact form, test it locally, and everything looks fine.
The fields are working. Validation is working. The submit button does something.
Then you deploy the website.
Suddenly, submissions aren't reaching your inbox. Sometimes the browser shows an error. Other times, the form appears to submit successfully, but nothing happens.
If you've worked on portfolio websites, landing pages, or small business websites, you've probably dealt with something like this.
The frustrating part is that the form UI is usually the easy part. The problems start when you need to send the data somewhere.
Here are seven mistakes worth checking before you start rewriting your form.
1. Your HTML form doesn't have a real destination
Consider this simple form:
<form>
<input
type="email"
name="email"
required
/>
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
It looks fine in the browser.
But where does the submitted data go?
Without an action attribute, the browser normally submits the form to the current page URL.
If that page doesn't have a backend capable of processing the request, nothing useful happens.
A form needs a destination that can receive its data.
For example:
<form
action="/api/contact"
method="POST"
>
<input type="email" name="email" required />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
This works only if /api/contact actually exists and supports POST requests.
If you're using a static website, such as one hosted on GitHub Pages or Cloudflare Pages, you need to provide a working submission endpoint separately.
The takeaway: A working form UI doesn't mean you have a working submission backend.
2. Your fetch request sends the wrong Content-Type
This is an easy mistake to make when working with JavaScript.
You might write:
const formData = new FormData(form);
await fetch("/api/contact", {
method: "POST",
body: formData
});
This is valid JavaScript, but it sends a multipart/form-data request.
Not every form submission API supports that content type.
Some endpoints only accept JSON or URL-encoded form data.
If the server doesn't support what you're sending, it may respond with:
415 Unsupported Media Type
One solution is to convert your form data into JSON:
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
This works when your endpoint accepts JSON and the fields are simple text values.
Another option is URL-encoded data:
const body = new URLSearchParams(
new FormData(form)
);
await fetch("/api/contact", {
method: "POST",
body
});
The important thing is to check the API documentation before deciding which format to send.
Also, don't manually set a multipart Content-Type header when sending FormData. The browser needs to generate the boundary itself.
3. You're ignoring CORS errors
You open the browser console and see something like:
Access to fetch has been blocked by CORS policy
It's tempting to immediately change your frontend code.
But CORS is primarily controlled by the server receiving the request.
For example, your frontend might run at:
https://myportfolio.com
While your submission API runs at:
https://api.example.com
These are different origins.
If the API doesn't allow requests from your website's origin, the browser can block access to the response.
The proper fix is usually to configure the API to allow the required origins, HTTP methods, and headers.
Don't try to fix it with:
mode: "no-cors"
That doesn't solve the underlying problem. It prevents your JavaScript from reading the response.
Also, not every apparent CORS error is caused by a CORS configuration issue. A wrong URL, failed preflight request, or server error can produce similar symptoms.
Check the Network tab before making changes.
4. Your JavaScript redirects or reloads the page
Here's another common mistake:
form.addEventListener("submit", async () => {
await fetch("/api/contact", {
method: "POST"
});
});
The problem is that the browser still performs the form's default submission.
That can reload or navigate away from the page before your JavaScript request has finished.
When handling submission with JavaScript, prevent the default behavior:
form.addEventListener("submit", async (event) => {
event.preventDefault();
// Handle the submission here
});
Now your JavaScript controls the request and the user experience.
One more thing: if you're using a plain HTML form with an action and method, you don't necessarily need JavaScript at all.
Sometimes the simplest solution is the better one.
5. You're showing success before checking the response
I see this pattern quite often:
await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(data)
});
alert("Message sent successfully!");
The problem?
fetch() doesn't reject its promise just because the server returns a 400, 429, or 500 HTTP status.
So your UI could show a success message even when the server rejected the request.
A better approach:
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(
`Submission failed: ${response.status}`
);
}
alert("Your message was received!");
} catch (error) {
console.error(error);
alert("Something went wrong. Please try again.");
}
This is a small improvement, but it makes debugging much easier.
Remember that an HTTP success response may only mean the server accepted the submission. Email notifications and other background tasks can still fail later.
6. You forgot about spam protection
Your contact form finally works.
A few days later, your inbox is full of messages advertising random products and suspicious links.
Welcome to contact form spam.
Public form endpoints are easy for bots to discover.
You can reduce spam with several techniques:
- Honeypot fields: Hidden fields that normal visitors should leave empty.
- Rate limiting: Restrict how frequently a client can submit.
- Server-side validation: Check incoming data instead of trusting the browser.
- Spam filtering: Identify and filter suspicious submissions.
None of these techniques is perfect on its own.
For example, client-side validation can be bypassed. A bot doesn't need to use your actual website to send a POST request.
That's why spam protection should happen on the receiving side, too.
7. You're building a backend for something that doesn't need one
This is probably the most interesting one.
Imagine you're building a simple portfolio website.
You need a contact form with three fields:
- Name
- Message
You could create an Express API, connect a database, configure an email provider, deploy the service, and maintain it.
That approach makes sense when your application needs custom business logic.
But for a basic contact form?
It can be more infrastructure than you actually need.
A hosted form backend is another option.
For example, with Formboost, you can create a submission endpoint and connect it directly to your existing HTML form:
<form
action="https://formboost.app/f/YOUR_ENDPOINT_ID"
method="POST"
>
<label for="email">Email</label>
<input
id="email"
type="email"
name="email"
required
/>
<label for="message">Message</label>
<textarea
id="message"
name="message"
required
></textarea>
<button type="submit">
Send message
</button>
</form>
Replace YOUR_ENDPOINT_ID with your actual endpoint ID.
That's it for the basic HTML integration.
Formboost handles submission storage, email notifications, and spam screening on the receiving side.
You can keep your existing frontend without maintaining a separate server just for form submissions.
If you need webhooks or other integrations, those are available too, depending on your plan.
The trade-off is that you're depending on an external service. If you need complex custom processing, sensitive workflows with special compliance requirements, or complete control over storage and delivery, a backend you own may be the better choice.
A quick debugging checklist
Before deploying your next contact form, check these things:
- Does the form have a valid submission destination?
- Is the endpoint expecting JSON, URL-encoded data, or multipart data?
- Does the server permit requests from your frontend origin?
- Are you preventing the default browser submission when using JavaScript?
- Are you checking HTTP errors before showing success?
- Do you have server-side validation and spam protection?
- Do you actually need to maintain a custom backend?
Most form submission problems become easier to debug once you separate the frontend, the HTTP request, and the receiving backend.
Final thoughts
A contact form looks like a small feature, but there are quite a few things happening behind the submit button.
The UI is just one part.
You also need reliable submission handling, validation, error responses, spam protection, and a way to know whether the message actually arrived.
Whether you use your own Node.js API or a hosted service like Formboost, understanding these basics will save you debugging time.
I'm curious: what's the most annoying form submission bug you've encountered in production?
If you're working on a static site or landing page and don't want to maintain a separate submission API, you can explore Formboost or check out its API documentation.
Top comments (0)