Build a Secure Flask API with MongoDB

Build a Flask app with a JSON API and MongoDB form using secure secrets.

Introduction

30 Second Summary

Every web app handles sensitive data behind the scenes. One leaked database password or exposed API key can compromise an entire system.

In this project, you will build a Flask web application that reads data from a file, writes form submissions to a cloud database, and keeps secrets secure using DevSecOps practices.

What You'll Build

When you are done, you can open a browser tab, hit your /api endpoint to see live JSON data, and fill out a form that writes directly to your cloud database.

By the end of this project, you'll have:

  • A live API endpoint you can visit in any browser to get back a JSON list of items read from your backend file.
  • A browser form you can fill out to write data into MongoDB Atlas, with a success page confirming each submission.
  • Secure error handling that shows users a friendly message when something breaks, without leaking database details or stack traces.
  • Secret Mission: Add server-side input validation and sanitization to prevent bad data from reaching the database.

Are there any prerequisites?

You need basic Python knowledge and a code editor (this project uses VS Code). You also need a free MongoDB Atlas account, which you can create at mongodb.com/atlas.

No prior Flask or database experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the goal before any hands-on work begins.

Set Up Your Flask Project

Before you can build routes or connect to a database, you need a working Python environment with Flask installed.

In this step, you will create a project folder, set up a virtual environment, install your dependencies, and confirm Flask runs. Getting this right now means no surprises later.

In this step, get ready to:
  • Create a virtual environment and install Flask, Flask-PyMongo, and python-dotenv.
  • Build a minimal Flask app and see it running in your browser.
  • Set up the project file structure for templates, data, and secrets.
Create a virtual environment and install dependencies

A virtual environment isolates your project's packages from the rest of your system. This way, the specific versions you install here will not conflict with anything else on your machine.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir flask-mongo-app
cd flask-mongo-app

Your terminal prompt now shows flask-mongo-app as the current directory.

  • Create a Python virtual environment by running:
python -m venv venv
  • Confirm the venv folder was created by running:
ls

You should see a venv folder listed in the output.

Don't see venv in the output?

Make sure you ran python -m venv venv from inside the flask-mongo-app folder. If your system uses python3 instead of python, try python3 -m venv venv instead.

Need more help? Help me troubleshoot my virtual environment creation

  • Activate the virtual environment so your terminal uses the project's isolated Python:

macOS

. venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

  • Install Flask, Flask-PyMongo, and python-dotenv by running:
pip install Flask==3.1.3 Flask-PyMongo==3.0.1 python-dotenv==1.2.2

What are these packages?

  • Flask is the web framework that handles routing and responses.
  • Flask-PyMongo connects Flask to MongoDB so you can read and write data.
  • python-dotenv loads secrets from a .env file into your app without hardcoding them.
  • Verify Flask installed correctly by running:
flask --version

You should see output that includes Flask 3.1.3 along with your Python version.

Seeing command not found?

Make sure your virtual environment is active. You should see (venv) at the start of your prompt. If not, re-run the activation command above.

Still stuck? Help me fix my Flask installation

Create the app skeleton and verify Flask runs

With Flask installed, you can create a minimal app file to prove everything works end-to-end.

  • Open VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the flask-mongo-app folder on your Desktop, then click Open.
  • Create a new file called app.py by clicking the new file icon in the Explorer sidebar (or press Cmd+N on macOS / Ctrl+N on Windows, then save as app.py).
  • Paste the following code into app.py:
from flask import Flask

# Create the Flask application instance
app = Flask(__name__)

# Define the index route
@app.route('/')
def index():
    return '<h1>Hello, World!</h1>'

What does this code do?

  • from flask import Flask imports the Flask class from the package you just installed.
  • app = Flask(__name__) creates your Flask application instance.
  • @app.route('/') tells Flask to call the index() function when someone visits the root URL.
  • The function returns a simple HTML heading as the response.
  • Save app.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch back to your terminal. Make sure you are still inside the flask-mongo-app folder with (venv) active.
  • Before you run your app, what do you think you will see in the terminal output?
  • Start the Flask development server by running:
flask --app app run --debug

What does this command do?

  • flask --app app tells Flask which file contains your application (app.py).
  • run starts the development server.
  • --debug enables auto-reload so the server restarts when you save changes.

This command takes over your terminal. You will not be able to type other commands in this terminal window until you stop the server with Ctrl+C.

  • Open your browser and visit http://127.0.0.1:5000.

You should see a large Hello, World! heading displayed in the browser.

Seeing a blank page or connection refused?

Check that the terminal still shows the server running (no error messages). Make sure the URL is exactly http://127.0.0.1:5000 with no extra characters.

If you see an import error, confirm you saved app.py in the flask-mongo-app folder (not inside venv/).

Still stuck? Help me get my Flask app running

Create the project file structure

Your app needs folders for HTML templates and data files, plus configuration files for secrets and version control.

  • Stop the Flask server by pressing Ctrl+C in your terminal.
  • Create the templates and data folders by running:

macOS

mkdir templates data

Windows

mkdir templates, data
  • Confirm the folders were created by running:
ls

You should see app.py, data, templates, and venv in the output.

  • Create the .gitignore file in VS Code. Click the new file icon in the Explorer sidebar and name it .gitignore.
  • Paste the following into .gitignore:
venv/
.env
__pycache__/

What does .gitignore do?

Any file or folder listed here will be excluded from version control. This keeps your virtual environment, secrets, and cached bytecode out of your repository.

  • Save .gitignore.
  • Create one more file called .env in the Explorer sidebar. Leave it empty for now and save it.

Why create an empty .env file now?

You will store your MongoDB connection string here in a later step. Creating the file now ensures your .gitignore is already protecting it before any secrets get added.

  • Confirm all your project files are in place by running this in the terminal:

macOS

ls -a

Windows

Get-ChildItem -Force

You should see .env, .gitignore, app.py, data, templates, and venv in the output.

Missing .env or .gitignore?

Files starting with a dot are hidden by default. Make sure you used ls -a (macOS) or Get-ChildItem -Force (Windows) to see them.

If the files are truly missing, create them in VS Code by clicking the new file icon and typing the full filename including the dot.

Need help? Help me find my hidden files

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here is what app.py should look like at this point:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>'

And here is .gitignore:

venv/
.env
__pycache__/

The .env file should exist but remain empty. The templates/ and data/ folders should be present but empty.

Your Flask environment is fully set up. Next up, you will build your first API route that reads from a JSON file and serves real data.

Build the JSON File API

Your Flask app is running and responding with a greeting. Now it's time to make it serve real data.

In this step, you'll create a JSON file containing DevSecOps tool descriptions, then build an API route that reads and serves that file. This is your app's first useful output.

In this step, get ready to:
  • Create a JSON data file with sample content.
  • Add an API route that reads the file and returns JSON.
  • Test the endpoint and observe what happens when the file is missing.
Create the backend JSON data file

Your API needs data to serve. You'll create a JSON file with a list of DevSecOps tools that the /api route will read and return.

  • Create a new file called items.json inside your data/ folder with the following content:
[
    {
        "name": "SAST",
        "description": "Static Application Security Testing - analyzes source code for vulnerabilities"
    },
    {
        "name": "DAST",
        "description": "Dynamic Application Security Testing - tests running applications for security flaws"
    },
    {
        "name": "SCA",
        "description": "Software Composition Analysis - scans dependencies for known vulnerabilities"
    }
]

What does this file contain?

This is a JSON array with three objects, each representing a DevSecOps tool. Each object has a name field (the tool's acronym) and a description field explaining what it does.

Your /api route will read this file and return its contents as a JSON response.

  • Save data/items.json.
  • Confirm the file exists by running:
ls data/

You should see items.json listed in the output.

Don't see items.json?

  • Make sure you saved the file inside the data/ folder, not in the project root.
  • Check the filename is exactly items.json with no extra spaces or typos.

Why isn't my items.json file showing up in the data folder?

Add the /api route to your app

Now you need a route that opens the JSON file, parses it, and returns it as a proper JSON HTTP response. This means updating your imports and adding a new function to app.py.

  • Replace the entire contents of app.py with the following code:
import os
import json

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/")
def index():
    # Updated homepage with navigation links
    return "<h1>Flask Mongo App</h1><p><a href='/api'>API</a> | <a href='/form'>Form</a></p>"


@app.route("/api")
def api():
    # Build the path to the JSON data file
    filepath = os.path.join("data", "items.json")
    # Open and read the file contents
    with open(filepath, "r") as f:
        data = json.load(f)
    # Return the data as a JSON response
    return jsonify(data)

What does this code do?

  • import os and import json bring in Python's built-in modules for file paths and JSON parsing.
  • jsonify is a Flask helper that converts Python data into a proper JSON HTTP response with the correct content type.
  • The index() function now returns links to /api and /form so you can navigate between routes. The /form link will work once you build it in the next step.
  • os.path.join("data", "items.json") builds a cross-platform file path to your data file.
  • json.load(f) reads the file and parses the JSON into a Python list.
  • Save app.py. Your Flask dev server running in debug mode will detect the change and auto-reload.

✔️ Awesome, I've got everything!

Great. Make sure you've saved app.py and confirmed the terminal shows the server reloading.

ⓧ I'd like to double check the full code

Here is the complete app.py at this point:

import os
import json

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/")
def index():
    return "<h1>Flask Mongo App</h1><p><a href='/api'>API</a> | <a href='/form'>Form</a></p>"


@app.route("/api")
def api():
    filepath = os.path.join("data", "items.json")
    with open(filepath, "r") as f:
        data = json.load(f)
    return jsonify(data)
Test the endpoint

Time to see your API in action. Before you check the browser, think about what you expect to see at the /api URL.

  • Open your browser and visit http://127.0.0.1:5000/api.

You should see the full JSON list of three DevSecOps tools displayed in your browser. Each tool has a name and description field.

Seeing a 404 Not Found?

  • Check the URL is exactly http://127.0.0.1:5000/api with no trailing slash or typos.
  • Confirm your terminal shows the server reloaded after you saved app.py. If not, save the file again.
  • Make sure the @app.route("/api") decorator is directly above the def api(): function with no blank lines separating them.

Why am I getting a 404 when I visit /api?

Now here is a question. What do you think happens if items.json doesn't exist?

  • Temporarily rename the data file by running:
mv data/items.json data/items_backup.json
  • Refresh http://127.0.0.1:5000/api in your browser.

Flask shows an interactive debugger page with a full traceback. The error includes FileNotFoundError and exposes your file path and internal code structure. In a production app, this level of detail would be a security risk.

Why does this matter?

Right now, any unhandled error leaks internal details to anyone who visits your app. File paths, line numbers, and code snippets are all visible on that debug page. In DevSecOps, you never expose this to end users. You'll fix this in a later step by adding proper error handling.

  • Restore the file by running:
mv data/items_backup.json data/items.json
  • Refresh http://127.0.0.1:5000/api one more time to confirm the JSON list is back.

Your API endpoint works. It reads a file and returns structured data. Next up, you'll connect an HTML form to MongoDB Atlas so users can write data into a database.

Connect the Form to MongoDB Atlas

Your /api route proves Flask can read data and serve it as JSON. That handles the "read" side of things.

Now you need to write data too. In this step, you will build an HTML form that submits entries directly to MongoDB Atlas. To get things working fast, you will hardcode the connection string right in your source code.

In this step, get ready to:
  • Create an HTML form with name and description fields.
  • Connect Flask to MongoDB Atlas using Flask-PyMongo.
  • Add a success page that confirms each submission.
Create the HTML form template

Flask uses Jinja2 templates stored in a templates/ folder. You already created that folder in Step 1.

  • Create a new file called form.html inside your templates/ folder and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Submit Data</title>
</head>
<body>
    <h1>Submit New Entry</h1>
    <form action="/submit" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
        <label for="description">Description:</label>
        <textarea id="description" name="description" rows="4"></textarea>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

What does this template do?

The form has two fields: name and description. When the user clicks Submit, the browser sends a POST request to /submit with both values in the request body.

The name attribute on each input determines the key Flask uses to read the data on the server side.

  • Save templates/form.html.
  • Confirm the file exists by running:
ls templates/

You should see form.html listed in the output.

Don't see form.html?

  • Make sure you created the file inside the templates/ folder, not in the project root.
  • Check that your terminal is in the flask-mongo-app folder. Run pwd to confirm.

help me find my form.html file

Connect Flask to MongoDB Atlas

To write form data to a database, you need to connect Flask to your MongoDB Atlas cluster. First, grab your connection string from the Atlas dashboard.

  • Open MongoDB Atlas in your browser.
  • Navigate to the Database section in the left sidebar and find your cluster.
  • Click Connect on your cluster.
  • Select Drivers from the connection options.
  • Choose Python from the Driver dropdown.
  • Copy the connection string displayed. It looks like mongodb+srv://<username>:<db_password>@cluster0.xxxxx.mongodb.net/.
  • Replace <db_password> with your database user's actual password.

Add your IP to the access list

If prompted during the connection flow, click Add Your Current IP Address. Atlas blocks all connections by default, so your machine must be on the IP Access List.

What is a connection string?

A connection string tells your application where the database lives and how to authenticate. The mongodb+srv:// prefix uses DNS to automatically discover all the nodes in your cluster.

The format is: mongodb+srv://username:password@cluster-host/database?options.

Now update app.py to use the connection string and serve the form.

  • In app.py, find this import line at the top:
from flask import Flask, jsonify
  • Replace it with these two import lines:
from flask import Flask, jsonify, render_template, request, redirect, url_for
from flask_pymongo import PyMongo
  • Directly below the app = Flask(__name__) line, add the MongoDB configuration and the /form route by pasting this code:
# Connect to MongoDB Atlas (hardcoded for now)
app.config['MONGO_URI'] = 'mongodb+srv://your-username:your-password@your-cluster.mongodb.net/flask_app?retryWrites=true&w=majority'

mongo = PyMongo(app)


@app.route('/form')
def form():
    return render_template('form.html')

What does this code do?

  • app.config['MONGO_URI'] tells Flask-PyMongo where your Atlas cluster lives and which database to use (flask_app).
  • mongo = PyMongo(app) creates the database connection object you will use to read and write data.
  • The /form route renders form.html so users can see the form in their browser.
  • Replace your-username, your-password, and your-cluster in the connection string with the values from your Atlas connection string.
  • Save app.py.
  • Restart your Flask app by running:
flask --app app run --debug
  • Open http://127.0.0.1:5000/form in your browser.

You should see a page titled "Submit New Entry" with two input fields and a Submit button.

Seeing an import error or connection timeout?

  • If you see ModuleNotFoundError, confirm your virtual environment is activated and that you installed Flask-PyMongo in Step 1.
  • If the page hangs or you see a timeout error, confirm your IP address is in the Atlas IP Access List and that you replaced the placeholder credentials in the connection string.

help me fix my Flask-PyMongo connection

Add the success redirect

The form displays, but clicking Submit does nothing yet. You need a /submit route to handle the POST request and a success page to confirm the write.

  • Create a new file called success.html inside your templates/ folder and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Success</title>
</head>
<body>
    <h1>Data submitted successfully</h1>
    <p><a href="/form">Submit another entry</a> | <a href="/api">View API</a></p>
</body>
</html>

What does this template do?

This is the confirmation page the user lands on after a successful form submission. It provides links to submit another entry or check the API endpoint.

  • Save templates/success.html.
  • In app.py, add the /submit and /success routes below your /form route by pasting this code:
@app.route('/submit', methods=['POST'])
def submit():
    # Read the form fields from the POST request
    name = request.form.get('name', '')
    description = request.form.get('description', '')
    # Write the data to MongoDB Atlas
    mongo.db.entries.insert_one({'name': name, 'description': description})
    return redirect(url_for('success'))


@app.route('/success')
def success():
    return render_template('success.html')

What does this code do?

  • request.form.get('name', '') reads the value the user typed into the Name field.
  • mongo.db.entries.insert_one(...) creates a new document in the entries collection of your flask_app database.
  • redirect(url_for('success')) sends the browser to the /success page after a successful insert.
  • Save app.py.

Before you test, what do you think will happen when you fill in the form and click Submit? Will you land on a success page, or will something go wrong?

  • Return to http://127.0.0.1:5000/form in your browser.
  • Type Secrets Scanner in the Name field.
  • Type Detects hardcoded credentials in source code in the Description field.
  • Click Submit.

You should land on a page that says "Data submitted successfully" with links to submit another entry or view the API.

  • Now verify the document reached Atlas. Open MongoDB Atlas in your browser.
  • Click Database in the left sidebar, then click Browse Collections on your cluster.
  • Select the flask_app database, then click the entries collection.

You should see a document with name: "Secrets Scanner" and description: "Detects hardcoded credentials in source code". Your form writes data end-to-end.

Form submission hangs or shows an error?

  • Double-check that you replaced all three placeholders (your-username, your-password, your-cluster) in the connection string with your actual Atlas values.
  • If your password contains special characters like @ or #, you need to URL-encode them (for example, @ becomes %40).
  • Make sure your Atlas cluster is on the free tier (M0) and the IP Access List includes your current IP address.

help me debug my MongoDB Atlas form submission

✔️ Awesome, I've got everything!

Great. Double check you have saved app.py, templates/form.html, and templates/success.html.

ⓧ I'd like to double check the full code

Your app.py should contain these sections in order. Scroll up and confirm each one is present:

  • Import lines: os, json, Flask utilities (render_template, request, redirect, url_for), and PyMongo.
  • App creation, MONGO_URI config with your Atlas connection string, and mongo = PyMongo(app).
  • The / index route (unchanged from before).
  • The /api route reading data/items.json (unchanged from before).
  • The /form GET route rendering form.html.
  • The /submit POST route calling mongo.db.entries.insert_one() then redirecting to success.
  • The /success route rendering success.html.

Here is templates/form.html for reference:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Submit Data</title>
</head>
<body>
    <h1>Submit New Entry</h1>
    <form action="/submit" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
        <label for="description">Description:</label>
        <textarea id="description" name="description" rows="4"></textarea>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

And templates/success.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Success</title>
</head>
<body>
    <h1>Data submitted successfully</h1>
    <p><a href="/form">Submit another entry</a> | <a href="/api">View API</a></p>
</body>
</html>

Your form works end-to-end. Data flows from the browser, through Flask, and into MongoDB Atlas. Next up, you will add error handling and move that exposed connection string out of your source code.

Add Secure Error Handling

Your form submits data to MongoDB and your API returns JSON from a file. But what happens when something goes wrong?

If MongoDB is unreachable or the connection string is invalid, your app crashes and displays a full stack trace in the browser. That traceback shows internal file paths, your connection string, and your code structure. In DevSecOps, you never leak infrastructure details to end users.

In this step, get ready to:
  • Wrap database operations in try/except to catch errors gracefully.
  • Display user-friendly error messages inline on the form page.
  • Move your MongoDB connection string to a secure .env file.
Wrap database operations in try/except

Before fixing the problem, you need to see it firsthand. You will deliberately break your connection string to trigger the crash.

  • In app.py, find the line that sets MONGO_URI:
app.config['MONGO_URI'] = 'mongodb+srv://your-username:your-password@your-cluster.mongodb.net/flask_app?retryWrites=true&w=majority'
  • Add the word BROKEN somewhere in the middle of the connection string, so the line now looks like this:
app.config['MONGO_URI'] = 'mongodb+srv://your-username:your-password@BROKENyour-cluster.mongodb.net/flask_app?retryWrites=true&w=majority'
  • Save app.py. The dev server auto-reloads.
  • Open http://127.0.0.1:5000/form in your browser and fill in both fields with any values.

Before you click Submit, predict what you think will happen. Will the form submission succeed, show a nice error, or crash?

You see an interactive traceback page showing the full Python stack trace, local variables, and your code internals. This is Flask's built-in Werkzeug debugger exposing your file paths, connection string details, and application structure to anyone viewing the page.

Why is this dangerous?

In development, this debugger is helpful for you. In production, it would expose your database credentials, server paths, and code logic to attackers. The fix is to catch errors before they bubble up to Flask's default handler.

Python's try/except block lets you attempt a risky operation and handle the failure gracefully instead of crashing.

  • In app.py, replace the entire submit function (including the @app.route decorator) with the following code:
@app.route("/submit", methods=["POST"])
def submit():
    name = request.form.get("name", "")
    description = request.form.get("description", "")

    try:
        mongo.db.entries.insert_one({
            "name": name,
            "description": description
        })
        return redirect(url_for("success"))
    except Exception:
        error = "Something went wrong. Please try again."
        return render_template("form.html", error=error)

What does this code do?

  • The try block attempts the database insert. If it succeeds, the user is redirected to the success page as before.
  • The except Exception block catches any error during the insert. Instead of crashing, it stores a generic message in the error variable.
  • The generic message hides the real exception details from the user. An attacker learns nothing from "Something went wrong."
  • Save app.py.
  • Return to http://127.0.0.1:5000/form and submit the form again (the connection string is still broken).

This time, the page does not crash. The form re-renders without a stack trace. The error message is being passed to the template, but you cannot see it yet because the template does not display it.

Still seeing the debugger traceback?

Make sure you replaced the entire submit function, including the @app.route decorator line. Check that the try and except keywords are indented correctly inside the function.

Help me debug why my exception is not being caught

Display errors inline on the form

The error is being caught, but the user cannot see it. You need to update the HTML template to conditionally display the error message when one is passed.

  • Open templates/form.html and replace its entire contents with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Submit Data</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .error { color: red; background: #ffe0e0; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
        label { display: block; margin-top: 15px; font-weight: bold; }
        input, textarea { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; }
        button { margin-top: 20px; padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #0056b3; }
    </style>
</head>
<body>
    <h1>Submit New Entry</h1>
    {% if error %}
    <div class="error">{{ error }}</div>
    {% endif %}
    <form action="/submit" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
        <label for="description">Description:</label>
        <textarea id="description" name="description" rows="4"></textarea>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

What does this code do?

  • The {% if error %} block is Jinja2 template syntax. It only renders the error div when an error variable is passed from the route.
  • The .error CSS class styles the message with red text on a light red background, making it visually distinct from the rest of the form.
  • The <style> block adds basic layout and spacing so the form looks polished.
  • Save templates/form.html.
  • Refresh http://127.0.0.1:5000/form in your browser and submit the form again.

You now see a red error box at the top of the form displaying "Something went wrong. Please try again." The form did not crash, and no internal details were leaked.

Don't see the red error box?

Make sure the connection string in app.py is still broken (has BROKEN in it). If the connection string is valid, the insert succeeds and you get redirected to the success page instead.

Also confirm that the {% if error %} block in form.html is placed before the <form> tag, not inside it.

Help me figure out why my error message is not showing

While you are updating templates, also update templates/success.html with matching styling so both pages look consistent.

  • Open templates/success.html and replace its entire contents with the following:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Success</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
        .success { color: green; background: #e0ffe0; padding: 20px; border-radius: 4px; }
        a { color: #007bff; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="success">
        <h1>Data submitted successfully</h1>
        <p><a href="/form">Submit another entry</a> | <a href="/api">View API</a></p>
    </div>
</body>
</html>

What does this code do?

The .success class wraps the confirmation message in a green box, giving the user a clear visual signal that their submission worked. The link styles match the form page for a consistent look.

  • Save templates/success.html.
Move secrets to the .env file

The error handling works. But there is still a security problem hiding in plain sight: your MongoDB connection string is hardcoded directly in app.py.

If you ever push this code to GitHub, anyone can see your database credentials. In DevSecOps, secrets never live in source code. They live in environment variables that stay on your machine.

  • Open the .env file in your project folder and add your MongoDB connection string:
MONGO_URI=mongodb+srv://your-username:your-password@your-cluster.mongodb.net/flask_app?retryWrites=true&w=majority

What value should I put here?

Replace the placeholder with the same connection string you had hardcoded in app.py (remove the BROKEN part). Use your real Atlas username, password, and cluster address. Do not wrap the value in quotes.

  • Save the .env file.

Now update app.py to read the connection string from the environment instead of hardcoding it.

  • In app.py, replace everything from the top of the file down to mongo = PyMongo(app) with the following:
import os
import json
import re

from flask import Flask, jsonify, render_template, request, redirect, url_for
from flask_pymongo import PyMongo
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)
app.config["MONGO_URI"] = os.environ.get("MONGO_URI")

mongo = PyMongo(app)

What does this code do?

  • The from dotenv import load_dotenv line imports the function from the python-dotenv package you installed in Step 1.
  • Calling load_dotenv() reads the .env file and loads its values into the system environment variables.
  • The os.environ.get("MONGO_URI") call reads MONGO_URI from the environment. The hardcoded connection string is gone from your code.
  • The import re line imports Python's regex module. You will use this in the Secret Mission for input sanitization.
  • Save app.py.

Why is .gitignore important here?

Your .gitignore file already includes .env from Step 1. This means Git will never track or push your secrets file. Anyone cloning your repo gets the code without your credentials.

Time to verify the full flow. First, confirm successful submissions still work.

  • Restart the dev server by pressing Ctrl+C and running the following command:
flask --app app run --debug
  • Open http://127.0.0.1:5000/form in your browser.
  • Enter a name and description, then click Submit.

Do you think the submission will succeed this time, or will you see the error message again?

You should land on the success page showing "Data submitted successfully" in a green box. Your .env connection string is working.

  • Now test the error path. Open .env and add BROKEN into the connection string value.
  • Save .env and restart the dev server (press Ctrl+C then rerun flask --app app run --debug).
  • Go to http://127.0.0.1:5000/form and submit the form.

You see the red error box with "Something went wrong. Please try again." No stack trace. No leaked credentials.

  • Remove BROKEN from your .env connection string to restore it.
  • Save .env and restart the dev server.

Seeing a server startup error?

If Flask fails to start, check that your .env file has MONGO_URI= with no spaces around the equals sign and no quotes around the value. Also confirm that load_dotenv() is called before os.environ.get("MONGO_URI") in app.py.

Help me debug my .env and load_dotenv setup

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: app.py, templates/form.html, and .env.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point.

Your app.py should contain these sections in order:

  • Imports: os, json, re, then Flask imports, PyMongo, and load_dotenv.
  • The load_dotenv() call, followed by app config reading MONGO_URI from the environment.
  • The index, api, form, submit (with try/except), and success routes.

Your templates/form.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Submit Data</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .error { color: red; background: #ffe0e0; padding: 10px; border-radius: 4px; margin-bottom: 15px; }
        label { display: block; margin-top: 15px; font-weight: bold; }
        input, textarea { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; }
        button { margin-top: 20px; padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #0056b3; }
    </style>
</head>
<body>
    <h1>Submit New Entry</h1>
    {% if error %}
    <div class="error">{{ error }}</div>
    {% endif %}
    <form action="/submit" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name">
        <label for="description">Description:</label>
        <textarea id="description" name="description" rows="4"></textarea>
        <button type="submit">Submit</button>
    </form>
</body>
</html>

Your templates/success.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Success</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; text-align: center; }
        .success { color: green; background: #e0ffe0; padding: 20px; border-radius: 4px; }
        a { color: #007bff; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="success">
        <h1>Data submitted successfully</h1>
        <p><a href="/form">Submit another entry</a> | <a href="/api">View API</a></p>
    </div>
</body>
</html>

Your .env file (with your real credentials):

MONGO_URI=mongodb+srv://your-username:your-password@your-cluster.mongodb.net/flask_app?retryWrites=true&w=majority

Your app now handles errors gracefully and keeps secrets out of source code. Next up, you will extend this foundation with input validation to stop bad data before it reaches your database.

Secret mission

Add Input Validation and Sanitization

Your form accepts anything right now. Empty strings, HTML injection, absurdly long inputs. In DevSecOps, every piece of user input gets validated and sanitized before it touches a database. Can you add server-side checks that reject bad data and strip dangerous characters?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs on MongoDB Atlas Free tier, so there are no ongoing costs.

Resources you used:

  • MongoDB Atlas entries collection in the flask_app database (free tier, no charges).
  • Local project folder flask-mongo-app with Python virtual environment.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your Flask app.

  • Your Atlas free-tier cluster has no time limit and no charges. The entries collection stays available indefinitely.
  • Your local flask-mongo-app folder and virtual environment remain on your Desktop ready to use.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and Atlas data stay intact for when you return.

  • Stop the Flask dev server by pressing Ctrl+C in the terminal where it is running.
  • Deactivate your virtual environment by running:
deactivate
  • Your Atlas cluster and entries collection stay available at no cost. No action needed on Atlas.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your data and local files.

  • Drop the entries collection from MongoDB Atlas:
  • Log in to MongoDB Atlas and click Data Explorer under the Database heading in the sidebar.
  • Expand the flask_app database in the Connections sidebar.
  • Click the icon next to the entries collection, then click Drop Collection.
  • Type entries to confirm, then click Drop.
  • Delete the local project folder. On macOS/Linux:
cd ~/Desktop
rm -rf flask-mongo-app
  • On Windows:
cd ~\Desktop
rmdir /s /q flask-mongo-app

Nice Work!

Nice Work!

You just built a secure Flask web application from scratch, complete with a file-backed JSON API, a MongoDB-connected form, and proper DevSecOps practices protecting your secrets and your users.

Here's what you accomplished:

  • Built a /api endpoint that reads from a JSON file and serves structured data to any client.
  • Connected an HTML form to MongoDB Atlas for persistent data storage, with success redirects and inline error handling.
  • Secured your application using environment variables and try/except error handling so secrets and stack traces never leak to end users.
  • Secret Mission: Added input validation and sanitization to strip HTML tags and enforce length limits before data reaches the database.

Ready to quiz yourself?