Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Implementing Server-Side Validation

Recommended resources

Hello again! Welcome back to the course.

In our last few lessons, we've successfully built the core AI-powered backend for your writing assistant. You created endpoints for grammar correction and summarization, learning how to send user text to the Gemini API and return the results. However, our code currently makes a big assumption: that the user will always provide valid text.

This lesson addresses that assumption. Our goal is to implement server-side validation to handle invalid user inputs (e.g., empty text). We need to build a guard for our server. If a user tries to submit an empty request, our application should reject it gracefully with a clear error message instead of wasting an API call or crashing.

This is a crucial step in making our application robust and professional.

Server-Side Form Validation with Python and htmx
This image illustrates what we're aiming for. When a user submits invalid data, the server should be able to catch the errors and report them back. This ensures data quality and protects our application.

1. Why Server-Side Validation is Essential

In software development, there's a principle called "Garbage In, Garbage Out" (GIGO). If your application accepts bad input, it will likely produce bad output. More importantly, you should never trust data coming from a user. While we will later add checks in the browser (client-side validation), a user could bypass them. The server is your application's last line of defense.

By validating on the server, we:

  • Prevent Errors: Avoid sending empty or invalid data to the Gemini API, which could cause errors.
  • Save Resources: Prevent unnecessary, and potentially costly, API calls.
  • Improve Security: Protect against malicious inputs designed to break the application.
  • Provide Clear Feedback: Tell the user exactly what went wrong so they can fix it.

2. The Simple Approach: Conditional Checks

You've actually already seen a basic form of validation in the code from our previous lessons. Let's look at the summarize_endpoint again:

@app.route('/api/summarize', methods=['POST'])
def summarize_endpoint():
    user_text = request.json.get('text')

    # This is our simple validation check!
    if not user_text:
        return jsonify({'error': 'No text provided'}), 400

    # ... rest of the function ...

This if not user_text: block is our validator.

  • It checks if user_text is None or an empty string "".
  • If the check is true (the text is missing), it immediately stops and returns a JSON error message.
  • Crucially, it also returns the HTTP status code 400 Bad Request. This is the standard way to tell the client that the request it sent was flawed and could not be processed.

This approach works perfectly for a single, simple check. But what if we wanted to add more rules? For example:

  • The text must be at least 10 words long.
  • The text must be less than 5000 characters.
  • The user must provide a "tone" setting (e.g., 'formal', 'casual').

Adding many if/elif statements would make our function messy and hard to maintain. For a more scalable and organized solution, we can use a dedicated library.


3. The Professional Approach: Using Flask-WTF

The most popular way to handle forms and data validation in Flask is with the Flask-WTF extension, which builds on a powerful library called WTForms. It allows us to define our validation rules in a clean, reusable Python class.

This video from Codemy.com gives a great introduction to setting it up. We'll walk through the key steps.

Web Forms With WTF! - Flask Fridays #5

Watch this video, 'Web Forms With WTF!', to understand the fundamentals of setting up and using Flask-WTF.

Follow these three segments: Installation and Imports (1:30 - 4:04): See how to install flask-wtf and import the necessary components like FlaskForm, StringField, and DataRequired. Secret Key (4:37 - 6:03): Learn why a SECRET_KEY is necessary for security (specifically, CSRF protection). Creating a Form Class (6:03 - 7:41): Pay close attention to how a Python class is created to represent the form, with fields and validators.

Let's apply what you just saw to our project.

Step 1: Install and Configure Flask-WTF

First, stop your Flask server if it's running (Ctrl+C in the terminal) and install the library:

pip install flask-wtf

Next, as the video explained, Flask-WTF requires a SECRET_KEY to secure forms against attacks. Open your app.py file and add this configuration right after you create the Flask app instance:

# In app.py
from flask import Flask, request, jsonify
# ... other imports ...

app = Flask(__name__)
# Add this line!
app.config['SECRET_KEY'] = 'a-super-secret-key-that-is-hard-to-guess'

# ... rest of your code ...

Important: For a real application, this key should be a long, random string and should not be written directly in the code. We'll cover how to handle that securely in a later module on deployment. For now, this is fine.

Step 2: Create a Form Class for Validation

To keep our project organized, create a new file in your project's root directory named forms.py. This is where we will define all our validation forms.

Inside forms.py, add the following code:

# In forms.py
from flask_wtf import FlaskForm
from wtforms import TextAreaField
from wtforms.validators import DataRequired, Length

class TextSubmissionForm(FlaskForm):
    text = TextAreaField(
        'Text', 
        validators=[
            DataRequired(message="Text field cannot be empty."),
            Length(min=10, max=5000, message="Text must be between 10 and 5000 characters.")
        ]
    )

Code Breakdown:

  • We import FlaskForm (the base class), TextAreaField (a field for multi-line text), and our validators: DataRequired and Length.
  • We create a class TextSubmissionForm that inherits from FlaskForm.
  • We define a text field. The validators list attached to it tells Flask-WTF what rules to apply.
    • DataRequired(): Ensures the field is not submitted empty. We've added a custom error message.
    • Length(min=10, max=5000): Ensures the text is within a reasonable size range.

Step 3: Integrate the Form into Your Endpoint

Now, we'll refactor the /api/summarize endpoint in app.py to use our new form for validation.

# In app.py

# Make sure to import your new form class and other wtforms components
from forms import TextSubmissionForm
from wtforms.validators import ValidationError
# ... other imports ...


@app.route('/api/summarize', methods=['POST'])
def summarize_endpoint():
    # 1. Get the JSON data from the request
    incoming_data = request.get_json()
    if not incoming_data:
        return jsonify({'errors': {'json': 'Invalid JSON or no data provided.'}}), 400

    # 2. Instantiate the form with the incoming JSON data
    form = TextSubmissionForm(data=incoming_data)

    # 3. Run validation
    if form.validate():
        # Validation passed, we can safely access the data
        user_text = form.text.data
        prompt = (
            "Summarize the following text into a single, concise paragraph, "
            "capturing the main points and key information.\n\n"
            "Text: " + user_text
        )
        generation_config = genai.types.GenerationConfig(temperature=0.2)

        try:
            response = model.generate_content(prompt, generation_config=generation_config)
            summary_text = response.text
            return jsonify({'summary_text': summary_text.strip()})
        except Exception as e:
            print(f"An error occurred: {e}")
            return jsonify({'error': 'Failed to communicate with the AI model.'}), 500
    else:
        # 4. Validation failed, return the errors
        return jsonify({'errors': form.errors}), 400

Refactoring Breakdown:

  1. We get the entire JSON object from the request.
  2. We instantiate our TextSubmissionForm, passing the incoming_data to its data parameter. This is how WTForms populates its fields from a dictionary (like our JSON) instead of a standard web form.
  3. We call form.validate(). This method runs all the validators we defined (DataRequired, Length). It returns True if everything passes.
  4. If validation fails, form.validate() returns False. We then return a JSON response containing form.errors. This is a dictionary that maps field names to a list of their validation error messages, like {'text': ['Text must be between 10 and 5000 characters.']}. We also return the 400 Bad Request status code.

Now, try testing your updated endpoint in Postman or Insomnia.

  • Send an empty JSON body {}. You should get an error about the text field being empty.
  • Send a request with short text like {"text": "hi"}. You should get an error about the minimum length.
  • Send a valid request with a longer piece of text. It should work just as before.
Test your understanding!

Your colleague wants you to also validate the grammar correction endpoint (/api/correct-grammar). How would you modify that function to use the TextSubmissionForm you just created?

Show answer

You would apply the exact same validation logic. You would replace the simple if not user_text: check with the TextSubmissionForm instantiation and form.validate() block, just like you did for the summarization endpoint. This shows the power of reusable form classes!


Conclusion

Congratulations! You've made your application significantly more robust and professional. By implementing server-side validation, you've built a crucial line of defense that ensures data quality and protects your backend services.

Key Takeaways:

  • Always validate on the server. Never trust input from the client.
  • Simple if statements are fine for basic checks, but can become unmanageable.
  • Flask-WTF provides a clean, powerful, and reusable way to define and apply complex validation rules using form classes.
  • You can use WTForms to validate JSON API data by passing it to the form's data parameter and calling form.validate().
  • Returning a 400 Bad Request status code with a JSON body containing specific error messages is the standard way to handle validation failures in an API.

We now have a set of robust, AI-powered backend endpoints. In the next module, we'll shift our focus to the frontend and start building the user interface that will interact with these APIs. Our first step will be to structure the user interface with HTML to support multiple features using tabs or sections.