What we're trying to achieve
The goal of this how-to post is to walk you through how you can add client side validation to forms on the frontend, and when it's posted to the backend, run a second set of validations on the data to make sure it's valid.
We'll follow the princple of not trusting user data, which is why we run a second set of validations on the backend to verify that everything is as expected.
If there are any errors in the data, we'll send back a response that can be parsed by the frontend and displayed next to the correct form field that is the cause of the error.
The tools/libraries that we're going to be using for this
I'm a Python 3 & React developer, so we're going to be using Flask as our backend framework, and React for our frontend.
For building the forms on the frontend I'm going to use Formik, it's the best library out there to build forms in React at the moment. The documentation for it is excellent, it's well designed, and easy to add customise everything.
The backend will use Marshmallow, it's a library for serialization of objects, but it also contains support for validation - which we'll be using. We're also going to use webargs which allows us to "connect" Marshmallow to Flask, and valid the request data using a Marshallow schema.
What we're going to build
To show you how all the pieces connect together, we're going to build a simple signup form.
On the frontend, we'll check that the name, email address and password are not null, and correctly formatted. We'll check the password is at least 8 characters.
On the backend, we'll again check all the required fields are there, and verify the length of the password. Additionally we'll also check that the email address given is not already in use.
This is a nice example because every web app has this workflow!
Marshmallow schema
The first thing we're going to do is to define what our backend is expecting in how the data is "shaped" and the expected properties of the data we're sending to our signup endpoint.
We're going to use Marshmallow to define a schema which describes what the "create user" request body should look like
from marshmallow import Schema, fields, validate
class CreateUserSchema(Schema):
name = fields.String(required=True)
password = fields.String(required=True, validate=validate.Length(min=8))
email_address = fields.String(required=True, validate=validate.Email(), data_key='emailAddress')
create_user_schema = CreateUserSchema()
The things of interest are:
- The
required=True
parameter, this tells Marshmallow that the field is required in the request - The
validate=...
parameter, this allows you to validate the data in the field against rules - The
data_key='emailAddress'
parameter, this tells Marshmallow the name of the field in the request body to use when validating the data, for exampleemailAddress
rather thanemail_address
The signup endpoint
The next step is to create an endpoint that'll accept the the post request from our client. We'll create a simple endpoint for this:
from flask import Flask
app = Flask(__name__)
@app.route('/user/', methods=['POST'])
def create_user():
"""Create a new user"""
return 'User created'
if __name__ == '__main__':
app.run(debug=True)
At the moment this doesn't do anything exciting, it'll just responsed with "user created".
Connecting Marshmallow with Flask
What we really what to do now is get Marshmallow to load the request data we send this endpoint and validate it against our schema. To do that we're going to use a library called webargs. This is a library for parsing and validating HTTP requests, it has support for both Flask and Marshmallow.
So let's tie the two together:
from marshmallow import Schema, fields, validate
from webargs.flaskparser import use_args
from flask import Flask, jsonify
class CreateUserSchema(Schema):
name = fields.String(required=True)
password = fields.String(required=True, validate=validate.Length(min=8))
email_address = fields.String(required=True, validate=validate.Email(), data_key='emailAddress')
create_user_schema = CreateUserSchema()
app = Flask(__name__)
@app.route('/user/', methods=['POST'])
@use_args(create_user_schema)
def create_user(args):
"""Create a new user"""
return jsonify({
'message': 'User created'
}
if __name__ == '__main__':
app.run(debug=True)
Okay, now we're getting somewhere! This endpoint will now expect POST requests that are JSON encoded which match our schema.
What we've done here is add the use_args
decorator from webargs, and passed in an instance of our create user schema. We've also added an args
parameter to our create_user
route handler. This parameter will contain data from the request if it passes validation.
If we send the right request body, we'll get back the "user created" message, but if there's a problem with validation, then the endpoint will return a 422 Unprocessable Entity status code.
So if we send:
{
"name": "sanj",
"emailAddress": "hello@sanjeevan.co.uk",
"password": "password123"
}
We'll receive:
{
"message": "User created"
}
But if we are missing any fields, or fields on the schema fail validation, we'll get back just a 422 status code. This however isn't very useful, what we really want is a better description of what went wrong!
Better validation error response
When webargs runs the validation of the json request and it fails - it'll raise an error using Flask's abort()
function and passes along a dict. This dict will contain detailed information of what went wrong.
What we'll do is to intercept this error, and return a json response with details of the error. This is how we'll do it:
from marshmallow import Schema, fields, validate
from webargs.flaskparser import use_args
from webargs import ValidationError
from flask import Flask, jsonify
class CreateUserSchema(Schema):
name = fields.String(required=True)
password = fields.String(required=True, validate=validate.Length(min=8))
email_address = fields.String(required=True, validate=validate.Email(), data_key='emailAddress')
create_user_schema = CreateUserSchema()
app = Flask(__name__)
@app.errorhandler(422)
def validation_error(err):
"""Handles 422 errors"""
messages = err.data.get('messages').get('json')
return jsonify(messages)
@app.route('/user/', methods=['POST'])
@use_args(create_user_schema)
def create_user(args):
"""Create a new user"""
return jsonify({
'message': 'User created'
})
if __name__ == '__main__':
app.run(debug=True)
We've defined a new validation_error
function that uses Flask's errorhandler
decorator. This means when a route throws a 422 HTTP error, this function will be receive that exception and can return the appropiate response.
In this case it'll receive the dict which contains details about the error. We just extract the right fields and return it in the API response.
For example, let's send a request that is missing the password field:
{
"name": "sanj",
"emailAddress": "hello@sanjeevan.co.uk"
}
The response's status code will still be 422, but it'll contain better information about what caused the problem:
{
"password": [
"Missing data for required field."
]
}
Awesome, now we've got the backend validating data that's sent to our create user endpoint. Let's mock a signup form on our frontend and add some clientside validation!
The signup form
Let's put together a signup form that'll collect the name, email address and password of our user. To do this in Formik, is pretty simple:
RegisterForm.tsx
import React from 'react'
import { Formik, Field, Form, FormikHelpers } from 'formik'
interface Props {
onSubmit: (values: UserValues, helpers: FormikHelpers<UserValues>) => void | Promise<any>
}
export interface UserValues {
name: string
emailAddress: string
password: string
}
const initialValues: UserValues = {
name: '',
emailAddress: '',
password: ''
}
const RegisterForm: React.FC<Props> = ({ onSubmit }) => {
return (
<Formik<UserValues> onSubmit={onSubmit} initialValues={initialValues}>
{ () => (
<Form>
<Field
type="input"
name="name"
/>
<Field
type="input"
name="emailAddress"
/>
<Field
type="password"
name="password"
/>
<button type="submit">Create user</button>
</Form>
)}
</Formik>
)
}
export default RegisterForm
We've defined three fields here: name, password, and emailAddress. Notice that the names of these fields match the schema we've defined in Marshmallow.
This form is very basic - we haven't added any client side validation to this yet. That's the next step.
Adding clientside validation
To add validation to our form, we're going to use Yup which is a validation library, and hook that into Formik which supports the validation schema that Yup uses.
Let's define our schema in Yup:
import * as up from 'yup'
const schema = yup.object().shape({
name: yup.string().required(),
emailAddress: yup.string().email().required(),
password: yup.string().required().min(8)
})
This schema matches the one in Marshmallow.
We also need to display errors from Yup in the form, to do this we'll create a custom field component that'll display errors from validation
MyField.tsx
import React from 'react'
import { Field, useField, ErrorMessage } from 'formik'
const MyField = (props: any) => {
const [field, meta] = useField(props)
let klass = props.className
if (meta.error && meta.touched) {
klass = `${klass} is-invalid`
}
return (
<>
<Field {...field} {...props} className={klass} />
<ErrorMessage name={field.name} component="div" className="invalid-feedback" />
</>
)
}
This custom field will use the ErrorMessage
component to display error messages from our validation, and it'll add the right css classes so that we can highlight the field in red.
Nearly there, now lets use this new custom field on our form, and add validation using Yup, and make the form pretty using Bootstrap to style everything.
RegisterForm.tsx
import React from 'react'
import { Formik, Field, Form, FormikHelpers } from 'formik'
import * as yup from 'yup'
import MyField from './MyField'
interface Props {
onSubmit: (values: UserValues, helpers: FormikHelpers<UserValues>) => void | Promise<any>
}
export interface UserValues {
name: string
emailAddress: string
password: string
}
const schema = yup.object().shape({
name: yup.string().required(),
emailAddress: yup.string().email().required(),
password: yup.string().required().min(8)
})
const initialValues: UserValues = {
name: '',
emailAddress: '',
password: ''
}
const RegisterForm: React.FC<Props> = ({ onSubmit }) => {
return (
<Formik<UserValues> validationSchema={schema} onSubmit={onSubmit} initialValues={initialValues}>
{ () => (
<Form>
<div className="form-group">
<MyField
type="input"
name="name"
className="form-control"
placeholder="Your full name"
/>
</div>
<div className="form-group">
<MyField
type="input"
name="emailAddress"
className="form-control"
placeholder="Your email address"
/>
</div>
<div className="form-group">
<MyField
type="password"
name="password"
className="form-control"
placeholder="A password, at least 8 characters long"
/>
</div>
<div>
<button className="btn btn-primary" type="submit">Create user</button>
</div>
</Form>
)}
</Formik>
)
}
export default RegisterForm
We'll create a new component called App
that'll use our form. At the moment, when the form is submitted, we'll print out the form values to console.
App.tsx
import React from 'react';
import './App.css';
import RegisterForm, { UserValues } from './RegisterForm';
import { FormikHelpers } from 'formik';
import styled from 'styled-components';
const Wrapper = styled.div`
margin: 5rem auto;
width: 450px;
background-color: #fff;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: rgba(180, 180, 180, 0.14) 0px 3px 12px 0px, rgba(0, 0, 0, 0.14) 0px 1px 2px 0px;
`;
function App() {
const handleSubmit = (values: UserValues, helpers: FormikHelpers<UserValues>) => {
console.log(values)
}
return (
<div className="App">
<Wrapper>
<h3>Create your account</h3>
<p>Enter your details below to create a new account with Appy.</p>
<RegisterForm onSubmit={handleSubmit} />
</Wrapper>
</div>
);
}
export default App;
If the required fields, or validation fails on this form, it'll display the validation errors to the user:
Connecting the frontend from to the backend
Now that we've got clientside validation on our form working, let's wire that to our backend API endpoint:
import React from 'react';
import './App.css';
import RegisterForm, { UserValues } from './RegisterForm';
import { FormikHelpers } from 'formik';
import styled from 'styled-components';
import axios from 'axios'
const Wrapper = styled.div`
margin: 5rem auto;
width: 450px;
background-color: #fff;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: rgba(180, 180, 180, 0.14) 0px 3px 12px 0px, rgba(0, 0, 0, 0.14) 0px 1px 2px 0px;
`;
const registerUser = (values: UserValues) => {
return axios.post('http://localhost:5000/user/', values)
}
function App() {
const handleSubmit = async (values: UserValues, helpers: FormikHelpers<UserValues>) => {
try {
const response = await registerUser(values)
} catch (e) {
if (e.response.status === 422) {
for (const fieldKey in e.response.data) {
const errorMessages = e.response.data[fieldKey]
helpers.setFieldError(fieldKey, errorMessages[0])
}
}
}
}
return (
<div className="App">
<Wrapper>
<h3>Create your account</h3>
<p>Enter your details below to create a new account with Appy.</p>
<RegisterForm onSubmit={handleSubmit} />
</Wrapper>
</div>
);
}
export default App;
Additional backend validation
Now that the frontend and backend are talking to each other, let's add an additional validation check to the backend.
In our signup flow, we'll want to check that the email address is not already in use. If it is, we want to send back a validation error message that'll be show on the form.
To do this, we just make a few minor alternations to the our validation_error
handler:
from marshmallow import Schema, fields, validate, ValidationError
from webargs.flaskparser import use_args
from flask import Flask, jsonify
from flask_cors import CORS
class CreateUserSchema(Schema):
name = fields.String(required=True)
password = fields.String(required=True, validate=validate.Length(min=8))
email_address = fields.String(required=True, validate=validate.Email(), data_key='emailAddress')
create_user_schema = CreateUserSchema()
app = Flask(__name__)
CORS(app)
@app.errorhandler(422)
@app.errorhandler(ValidationError)
def validation_error(err):
"""Handles 422 errors"""
if isinstance(err, ValidationError):
return err.normalized_messages(), 422
else:
messages = err.data.get('messages').get('json')
return jsonify(messages), 422
@app.route('/user/', methods=['POST'])
@use_args(create_user_schema)
def create_user(args):
"""Create a new user"""
user = find_user_by_email_address(args['email_address'])
if user:
raise ValidationError('That email is already in use', "emailAddress")
return jsonify({
'message': 'User created'
})
def find_user_by_email_address(email):
# add your custom code here...
return True
if __name__ == '__main__':
app.run(debug=True)
In this example we raise a ValidationError
with the appropiate message and the field name that we want the error to be displayed in. We also update the validation_error
function to handle the ValidationError
exception.
So now the error message will be automatically shown in our frontend form:
Wrapping up
And that's all there is to it.
By combining some really great opensource libraries we can define the schema of the requests we're expecting for our endpoints, do data validation on the backend, and frontend, and build an easy mechanisam for surfacing validation errors on the backend to our frontend app.