How to Add Server-Side Form Validation in Flask
Client-side validation is only a convenience - anyone can send a request straight to your API. Validate every field on the server before you write to the database: check required fields and lengths, check the email format, collect all errors, and return 400 with the list. A valid request still returns 201.
Why the server must validate
A form in the browser can check fields with required or JavaScript, but the
API behind it receives raw HTTP. curl, scripts and spam bots never run your
front-end code. If the endpoint inserts whatever arrives, junk rows, empty
names and broken email addresses end up in the database.
Here is an endpoint with no validation at all:
@app.post("/contacts")
def create_contact():
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Request body must be JSON"}), 400
name = data.get("name", "")
email = data.get("email", "")
message = data.get("message", "")
cur.execute(
"INSERT INTO contacts (name, email, message) VALUES (%s, %s, %s) RETURNING id",
(name, email, message),
)
...
return jsonify({"id": new_id}), 201
Any JSON object passes, including {} with all three fields missing.
Define the rules first
Write the rules down before you write code. For a contact form:
| Field | Rule |
|---|---|
name |
required, at least 2 characters |
email |
required, local@domain.tld shape |
message |
required, at least 10 characters |
An empty string counts as missing. If any rule fails, the response is 400
with a JSON body that lists every error.
Add the checks before the insert
import re
EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
@app.post("/contacts")
def create_contact():
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "Request body must be JSON"}), 400
name = (data.get("name") or "").strip()
email = (data.get("email") or "").strip()
message = (data.get("message") or "").strip()
errors = []
if len(name) < 2:
errors.append({"field": "name", "error": "name is required, min 2 characters"})
if not EMAIL_RE.match(email):
errors.append({"field": "email", "error": "valid email is required"})
if len(message) < 10:
errors.append({"field": "message", "error": "message is required, min 10 characters"})
if errors:
return jsonify({"error": "Validation failed", "details": errors}), 400
# only valid data reaches the database
cur.execute(
"INSERT INTO contacts (name, email, message) VALUES (%s, %s, %s) RETURNING id",
(name, email, message),
)
...
return jsonify({"id": new_id}), 201
A few details matter here:
data.get("name") or ""turns both a missing key and a JSONnullinto an empty string, so.strip()never fails onNone..strip()stops a name of two spaces from counting as two characters.- Collect all errors instead of returning on the first one. The user can correct every field in one try instead of one field per request.
- The
detailslist gives the front end a field name for each error, so it can show the message next to the right input.
About the email check
The regex above checks the shape: something before the @, a domain, a dot,
and a top-level part of two or more letters. missing@domain fails because
there is no dot in the domain part. No regex can tell you that an address
really exists - only sending an email can do that. Keep the check simple and
readable. If you need stricter rules, the email-validator package handles
the full syntax and can also check the domain.
Test both paths
# 400 - email has no dot in the domain
curl -i -X POST localhost:8000/contacts \
-H 'Content-Type: application/json' \
-d '{"name":"Jane Doe","email":"missing@domain","message":"This is long enough."}'
# 400 - every field is wrong, three errors in details
curl -i -X POST localhost:8000/contacts \
-H 'Content-Type: application/json' \
-d '{"name":"J","email":"","message":"short"}'
# 201 - valid
curl -i -X POST localhost:8000/contacts \
-H 'Content-Type: application/json' \
-d '{"name":"Jane Doe","email":"jane@example.com","message":"Hello, I have a question."}'
-i prints the status line, so you can see 400 BAD REQUEST or 201 CREATED
without reading the body.
When to use a library instead
Hand-written checks are fine for a few fields. When an API has many endpoints or nested objects, a schema library keeps the rules in one place: Pydantic, marshmallow, or Flask-WTF for HTML forms. They do the same job - check types, required fields and limits, then return a list of errors - with less repeated code. The rule stays the same: validate on the server, before the data is used.
Validation is not the same as escaping
Validation rejects data that breaks your rules. It does not make data safe to
put in SQL or HTML. Keep using parameterized queries (the %s placeholders
above) and escape output in templates, even for fields that passed validation.
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - freeWhat you'll practice
- Adding required-field, length and email-format checks to a Flask endpoint
- Collecting all validation errors into one structured 400 response
- Treating missing keys, null and empty strings the same way
- Testing the failure and success paths with curl -i
FAQ
Is client-side validation enough?
No. Client-side checks only help users who use your form. Anyone can send a request directly to the API with curl or a script, so the server must check every field again before it stores anything.
What status code should a validation error return?
400 Bad Request is the standard choice for invalid input. Some APIs use 422 Unprocessable Entity for input that is valid JSON but breaks a business rule. Pick one and use it on every endpoint.
How do I validate an email address in Python?
For a simple shape check, use a regex such as ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$. For full syntax rules, use the email-validator package. Only a confirmation email proves that the address really exists.
Should I return the first validation error or all of them?
Return all of them in a list, each with the field name. The user can then correct every field in one try, and the front end can show each message next to its input.
Keep learning
Do this project yourself. It's ready in a live cloud workspace in your browser - when you finish, it lands on your portfolio.
Start this project - free