Skip to main content

How to Save Contact Messages in JSON Using Flask?

TinyDB

In Part One, we built and styled our contact form using Flask and CKEditor. Now in Part Two, we’ll take it a step further by connecting the form to TinyDB so we can actually store user submissions.

In this tutorial, you’ll learn how to save form data into a JSON database, handle validation properly, and implement a clean redirect after submission. By the end, your contact form won’t just collect messages—it will securely store them like a real web application.

Prerequisite:

This tutorial is part of the Flask CKEditor Project Series.

📚 View the Complete Flask CKEditor Series

⬅ Previous Part                                                                                                      ➡ Next Part   

Preliminary
Before I begin, it is recommended that you activate the virtual environment and install the relevant dependencies.
python -m venv venv
venv\Scripts\activate 
pip install flask tinydb
Then, we need to set up the file and folder structure, as below:

TinyDB

Everything is the same as in part one, the templates folder, except for contact.html, which will now add a new thankyou.html. On the other hand, I need to amend the form.py and add two new routes to streamline the form submission in app.py! 

In fact, TinyDB is a non-schema database storage, and it stores all the input data in JSON format. Therefore, when the submission button is clicked, the data will be automatically stored in the message.json, as shown in the above diagram. 

The benefits are as follows:
  1. TinyDB is a pure Python database that stores data in a simple JSON file.
  2. It feels like working with Python dictionaries and lists.
  3. TinyDB is small and fast for low to medium data volumes.
  4. All data is stored in a readable .json file.
  5. You don’t need to learn SQL queries.
  6. TinyDB works smoothly inside Flask apps without needing external database services like MySQL or PostgreSQL

Step 1: Amend the form.py
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from flask_ckeditor import CKEditorField

class ContactForm(FlaskForm):
    name = StringField('Name:', validators=[DataRequired()])
    email = StringField('Email:', validators=[DataRequired()])
    subscribe = BooleanField("Subscribe to our newsletter?")
    message = CKEditorField('Message:', validators=[DataRequired()])
    submit = SubmitField("Submit ☺️")
In order to ensure the message field is accepted and validated under the WTF. I need to import the CKEditorField, assign it to the message field, and set the validator as data required.


Step 2: Update the submission form
Once I have amended the form.py, I need to update it on contact.html, as below:
<form method="POST" action="{{ url_for('submit') }}">
    {{ form.hidden_tag() }}

    <p>{{ form.name.label }} {{ form.name(size=32) }}</p>
    <p>{{ form.email.label }} {{ form.email(size=32) }}</p>
    <p>{{ form.message.label }} {{ form.message() }}</p>

    {{ ckeditor.load() }}
    {{ ckeditor.config(name='message') }}

    <p>{{ form.subscribe() }} {{ form.subscribe.label }}</p>
    <p class="submit-container">{{ form.submit(class="btn-submit") }}</p>
</form>
The form action refers to the submit() function on the app.py (we will set it up later on), and I am also adding an additional 'form.message()' widget on the contact.html. 

Even though I need to create a rich text editor by creating the ckeditor.load(). However, the 'form.message()' widget originally refers to a textarea by default. Therefore, when I coded a ckeditor.config with name='message', the purpose was to allow the rich text editor to replace the existing textarea widget display. 


Step 3: Set up the submit() function
Now I need to create the Submit() function as follows:
from flask import (Flask, render_template, redirect, 
                   url_for)
from tinydb import TinyDB

# The JSON file output path
db = TinyDB('flask_ckeditor/message.json')

def submit():
    # Create an instance of the ContactForm
    form = ContactForm()
    
    # Check if the form is submitted and passes validation
    
    if form.validate_on_submit():
    # Retrieve data from each form field
        name = form.name.data
        email = form.email.data
        subscribe = form.subscribe.data
        message = form.message.data
        
        # Insert the form data into the database
        db.insert({
            'name': name,
            'email': email,
            'subscribe': subscribe,
            'message': message
        })
        
        # Redirect the user to the thank you page after successful submission
        return redirect(url_for('thankyou'))
   
   # If form is not submitted or validation fails,
   # re-render the contact page and display validation errors
   return render_template('contact.html', form=form)
Let me import the relevant libraries and set the output path of message.json

What I did here:
1) I need to validate all the input fields (data required), such as name, email, subscribe, and message.
2) After validation, I need to add those input fields to the message.json database.
3) If the form is accepted and validated, the app will now redirect to the thankyou() function.
4) Otherwise, it will re-render on the same page.

The formatted message.json is as follows:
Json Formatter



Step 4: Set up the thankyou() function and thankyou.html
This function is to inform the user that the message was added successfully. 
The thankyou() is as follows:
@app.route('/thankyou')
def thankyou()
   # render to thankyou page
    return render_template('thankyou.html')
The thankyou.html is as follows:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Submitted Successfully</title>
  <style>
    h1 {
      colour: red;
      text-align: centre;
      font-family: Arial, sans-serif;
    }
  </style>
</head>
<body>
  <h1>Your message has been sent successfully! 😊</h1>
</body>
</html>
Here is the thankyou.html view:
thankyou.html

Bonus
If you need to view the details of the output on the terminal, simply add this line of code
print(db.all())

The details on the terminal are as follows:
Show a details




Final wrap-up:
We’ve now connected our Flask contact form to TinyDB and can store messages in a JSON file. The form validates input with Flask-WTF and uses CKEditor for rich text messages.
Submissions are saved securely, and users see a thank-you page after sending.
This setup makes your contact form fully functional and ready for small web apps or projects.
      
Published: Mar 2026
Last Updated: Mar 2026
---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Join 500+ Python learners and receive this printable Flask Cheat Sheet instantly. Plus, get new Flask tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

-----------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 


-----------------------------------------------------------------------------------------------------------------------------

About the Author

Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.