How to Create a Python web app with Authentication, and an API

What You Need

To follow along with this tutorial, you will need the following:

  • Python 3.10 or higher installed on your machine. You can download it from here.
  • A code editor of your choice. I recommend Visual Studio Code, which you can get from here.
  • A PostgreSQL database server running locally or on the cloud. You can learn how to set up one from here.

Creating the web app

We will use Flask, a popular Python web framework, to create our web app. Flask is lightweight, easy to use, and has many extensions for adding functionality. To install Flask, open a terminal and run the following command:

pip install Flask

Next, create a folder for your project and navigate to it. Then, create a file called app.py and paste the following code:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello, world!"

if __name__ == "__main__":
    app.run(debug=True)

This is a simple Flask app that returns “Hello, world!” when you visit the root URL. To run the app, run the following command in the terminal:

python app.py

You should see something like this:

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 123-456-789

Now, open your browser and go to http://127.0.0.1:5000/. You should see “Hello, world!” on the screen.

Congratulations! You have created your first Flask web app.

Adding authentication

To add authentication to our web app, we will use Flask-Login, an extension that provides user session management for Flask. Flask-Login handles the common tasks of logging in, logging out, and remembering user sessions.

To install Flask-Login, run the following command in the terminal:

pip install flask-login

Next, we need to create a User class that represents our users and implements some methods required by Flask-Login. Create a file called models.py and paste the following code:

from flask_login import UserMixin

class User(UserMixin):
    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = password

    def __repr__(self):
        return f"<User {self.username}>"

The User class inherits from UserMixin, which provides default implementations for some methods that Flask-Login needs. We also define an __init__ method that takes an id, a username, and a password as arguments and assigns them to instance attributes. Finally, we define a __repr__ method that returns a string representation of the user object.

Next, we need to create a login manager object that handles the login process for our app. Go back to app.py and add the following lines at the top:

from flask_login import LoginManager
from models import User

login_manager = LoginManager()
login_manager.init_app(app)

Here, we import LoginManager from flask_login and User from models. Then, we create a login manager object and initialize it with our app object.

Next, we need to tell Flask-Login how to load a user from its id. To do this, we use the @login_manager.user_loader decorator and define a function that takes an id as an argument and returns the corresponding user object. For simplicity, we will use a dictionary to store some dummy users in memory. Add the following lines below the login manager object:

users = {
    "1": User("1", "alice", "secret"),
    "2": User("2", "bob", "secret"),
    "3": User("3", "charlie", "secret")
}

@login_manager.user_loader
def load_user(user_id):
    return users.get(user_id)

Here, we create a dictionary called users that maps user ids to user objects. Then, we define a function called load_user that takes a user_id as an argument and returns the user object with that id from the users dictionary. If no such user exists, it returns None.

Next, we need to create some routes for logging in and logging out users. Add the following lines below the index route in app.py:

from flask import request, redirect, url_for, render_template
from flask_login import login_user, logout_user, login_required, current_user

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        for user in users.values():
            if user.username == username and user.password == password:
                login_user(user)
                return redirect(url_for("index"))
        return "Invalid username or password"
    else:
        return render_template("login.html")

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for("index"))

Here, we import some functions and decorators from flask, flask_login, and models. Then, we define a login route that handles both GET and POST requests. If the request method is POST, we get the username and password from the form data and loop through the users dictionary to find a matching user. If we find one, we call the login_user function to log in the user and redirect them to the index route. If we don’t find one, we return an error message. If the request method is GET, we render a template called login.html that contains a simple login form.

Next, we define a logout route that requires the user to be logged in. We use the @login_required decorator to enforce this. Inside the function, we call the logout_user function to log out the user and redirect them to the index route.

Next, we need to create a template folder and a login.html file inside it. Paste the following code in the login.html file:

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form method="POST">
        <p>Username: <input type="text" name="username"></p>
        <p>Password: <input type="password" name="password"></p>
        <p><input type="submit" value="Login"></p>
    </form>
</body>
</html>

This is a simple HTML template that contains a form with two input fields for username and password and a submit button.

Next we need to change the line where we create our flask application, so that it will know where to look for the template files, and be able to properly create sessions.

import os

app = Flask(__name__, template_folder=os.path.join(os.getcwd(), 'template'))
app.secret_key = "super secret key"

Here we are importing the os package to get the current directory, and adding the template folder name to it. Then we are setting the secret_key on the flask app, so that it can generate sessions on login.

Now, you can test your authentication system by running your app and visiting http://127.0.0.1:5000/login. You can use any of the dummy users to log in and then log out.

Adding an API

To add an API to our web app, we will use Flask-RESTful, an extension that provides a simple way to create RESTful APIs with Flask. Flask-RESTful handles the routing and parsing of requests and responses for us.

To install Flask-RESTful, run the following command in the terminal:

pip install flask-restful

Next, we need to create an API object that will register our resources and endpoints. Go back to app.py and add the following lines below the login manager object:

from flask_restful import Api

api = Api(app)

Here, we import Api from flask_restful and create an api object with our app object.

Next, we need to create some resources that will represent our data and logic. For this tutorial, we will create a simple Todo resource that will allow us to create, read, update, and delete todo items. Create a file called resources.py and paste the following code:

from flask_restful import Resource, reqparse
from flask_login import login_required

todos = {
    "1": {"task": "Buy groceries", "done": False},
    "2": {"task": "Do laundry", "done": True},
    "3": {"task": "Read a book", "done": False}
}

class TodoList(Resource):
    def __init__(self):
        self.parser = reqparse.RequestParser()
        self.parser.add_argument("task", type=str, required=True)
        self.parser.add_argument("done", type=bool)

    @login_required
    def get(self):
        return todos

    @login_required
    def post(self):
        args = self.parser.parse_args()
        todo_id = str(len(todos) + 1)
        todos[todo_id] = {"task": args["task"], "done": args.get("done", False)}
        return todos[todo_id], 201


class Todo(Resource):
    def __init__(self):
        self.parser = reqparse.RequestParser()
        self.parser.add_argument("task", type=str)
        self.parser.add_argument("done", type=bool)

    @login_required
    def get(self, todo_id):
        if todo_id not in todos:
            return {"message": "Todo not found"}, 404
        return todos[todo_id]

    @login_required
    def put(self, todo_id):
        if todo_id not in todos:
            return {"message": "Todo not found"}, 404
        args = self.parser.parse_args()
        todos[todo_id]["task"] = args.get("task", todos[todo_id]["task"])
        todos[todo_id]["done"] = args.get("done", todos[todo_id]["done"])
        return todos[todo_id], 200

    @login_required
    def delete(self, todo_id):
        if todo_id not in todos:
            return {"message": "Todo not found"}, 404
        del todos[todo_id]
        return {"message": "Todo deleted"}, 204

Here, we import Resource and reqparse from flask_restful and login_required from flask_login. Then, we create a dictionary called todos that stores some dummy todo items in memory. Each todo item has an id, a task, and a done status.

Next, we create a TodoList resource that represents the collection of todo items. We define an __init__ method that creates a parser object that will parse the request arguments for us. We add two arguments to the parser: task and done. The task argument is required and must be a string, while the done argument is optional and must be a boolean.

Next, we define a get method that returns the todos dictionary as a response. We use the @login_required decorator to require the user to be logged in to access this endpoint.

Next, we define a post method that creates a new todo item and adds it to the todos dictionary. We use the parser object to parse the request arguments and assign them to a variable called args. We generate a new todo id by adding one to the length of the todos dictionary and convert it to a string. We create a new todo item with the task and done arguments from the args variable and add it to the todos dictionary with the new id as the key. We return the new todo item and a status code of 201 as a response.

Next, we create a Todo resource that represents a single todo item. We define an __init__ method that creates another parser object with the same arguments as the TodoList resource.

Next, we define a get method that takes a todo id as a parameter and returns the corresponding todo item from the todos dictionary. If no such todo item exists, we return an error message and a status code of 404 as a response.

Next, we define a put method that takes a todo id as a parameter and updates the corresponding todo item in the todos dictionary. If no such todo item exists, we return an error message and a status code of 404 as a response. We use the parser object to parse the request arguments and assign them to a variable called args. We update the task and done attributes of the todo item with the values from the args variable if they are provided. We return the updated todo item and a status code of 200 as a response.

Next, we define a delete method that takes a todo id as a parameter and deletes the corresponding todo item from the todos dictionary. If no such todo item exists, we return an error message and a status code of 404 as a response. We use the del keyword to remove the todo item from the todos dictionary. We return a success message and a status code of 204 as a response.

Finally, we need to register our resources and endpoints with our api object. Go back to app.py and add the following lines at the bottom:

from resources import TodoList, Todo

api.add_resource(TodoList, "/todos")
api.add_resource(Todo, "/todos/<string:todo_id>")

Here, we import TodoList and Todo from resources. Then, we use the api object’s add_resource method to register our resources with their respective endpoints. The Todo resource takes a variable called todo_id in its endpoint.

Now, you can test your API by running your app and using tools like Postman or curl to send requests to http://127.0.0.1:5000/todos or http://127.0.0.1:5000/todos/. You will need to provide your login credentials in each request using basic authentication. And you can send your data using JSON requests.