Create a basic REST API in Python with Flask

NicolasBrondinBernard

Author
@NicolasBrondinBernard

Learn all the basics of Flask, by building a basic API to understand the mechanics of the framework!

Article published on 04/06/2025, last updated on 10/08/2026

In this tutorial, we're going to learn step by step how to create a REST API from scratch in Python with the Flask framework. This API will allow you to build a very simplified user management system, in order to better understand the mechanics and syntax of Flask.

We won't be using a database, all data will be managed in Python, to keep the tutorial simple.

Prerequisites

In order to properly follow and understand the rest of the content, you first need to:

  • Know the basics of Python
  • Understand the basics of the web (web server, HTTP requests,…)
  • Know what a REST API is

To follow this tutorial under the best conditions, also make sure that Python is installed on your machine with a version ≥ 3.11.1

Installation

Flask is a web framework for Python, it can be used to create websites/web applications, APIs, and generally anything that requires listening to HTTP requests and being able to respond to them.

First, we're going to install Flask, using the pip package manager:

$> pip install Flask

Once done, create a folder and give it a name, to house our API project. For example: mon-api-flask

My first API

In your project folder, create a main.py file, and paste in the following code:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

Here's what this code does:

  1. We initialize the Flask app, giving it the name of the module (here the __ name __ variable contains "main" because the name of our file is main.py)
  2. We declare a new route at the root (/), on which we'll listen for all HTTP GET requests (by default) on the defined url
  3. We define the controller logic of our route, like a classic function
  4. We return information (here a string) in the HTTP response, which will come with a 200 code (by default)

To test this example, you need to run our API, passing the name of our application, with the run command:

$> flask --app main run

You should see a message saying: Serving Flask app 'main' which means your API is running!

To test it, simply open the url http://127.0.0.1:5000 in your browser, and if you see "Hello World" appear, then the first step is successful!

Returning JSON

There's a good chance that you'd like your API to not just return plain text, but to be able to return objects in JSON format!

And that's great, because Flask makes this task much easier for us:

@app.route("/json")
def hello_json():
    return {"str1": "Hello", "str2": "world"}
		# or return ["hello", "world"]

And that's it! "Key-value" dictionaries (as well as lists) are automatically serialized to JSON by Flask.

Returning objects (classes)

Things get slightly more complicated when we want to return actual objects (meaning "class instances").

And yet, this is essential, because it's good practice when creating an API!

This allows:

  • Data validation (and keeping certain information hidden)
  • Keeping the same data model as in the rest of your application
  • Not having to go back through a key-value dictionary for each object

Let's take a simple fictional class:

class User:
	username: str
	firstname: str
	lastname: str
	password: str
	def __init__(self, username: str, firstname: str, lastname: str):
		self.username = username
		self.firstname = firstname
		self.lastname = lastname
		self.password = "azejbnzdfunIJIN65"

We can't directly turn its instances into JSON, because Python doesn't know how to do it from a complex object.

So we're going to use a data serialization/deserialization and validation library called Marshmallow

$> pip install marshmallow

And we're going to create a validator for our User class, so that marshmallow understands how to go from our User class to a JSON object, and vice-versa:

from marshmallow import Schema, fields

# [...]

class UserSchema(Schema):
    username = fields.Str(required=True)
    firstname = fields.Str(required=True)
    lastname = fields.Str(required=True)

# We create the validator from our schema
user_validator = UserSchema()

You'll have noticed that the password attribute is not present, because we never want to return it outside of our API… It's a safeguard!

Now let's add a new route to return our user, created using our User class, and transformed by marshmallow:

users = []
users.append(User("JohnDoe", "John", "Doe"))

# [...]

# description: returns the first user in the list
@app.route("/user")
def get_user():
    first_user = users[0]
		# dump(...) allows converting a class instance to json
    response = user_validator.dump(first_user)
    return response

Open http://127.0.0.1:5000/users and admire the result: your user, properly formatted (and without a password)!

Handling the different HTTP methods

An API isn't just used to retrieve data, but also to manage it, store it…

To follow the structure of a Rest API, we use the different HTTP methods (Get, Post, Put, Delete)

Flask lets us do this easily, directly in the route definition with the methods parameter:

# description: returns the list of all users
@app.route("/users", methods=['GET'])
def get_users():
	return "not implemented"

# description: adds a user to the list
@app.route("/users", methods=['POST'])
def create_user():
	return "not implemented"

The possible values are 'GET', 'POST', 'PUT' and 'DELETE'

Receiving data

Now that we know how to return all sorts of data in our API's responses, we need to learn how to retrieve the data received in requests!

There are two main ways to pass data in an HTTP request:

  • In the URL's parameters
  • In the request's body

In the request parameters

For URL parameters, it's very simple, you just need to read request.args:

from flask import Flask, request #new import (request)

# [...]

@app.route("/hello") # Example /hello?name=John
def hello_name():
    name = request.args.get("name")
    return "Hello, " + name

To test: http://127.0.0.1:5000/hello?name=John

In the request body

It's also possible to retrieve data sent as JSON in the request body for POST and PUT methods.

By default, the JSON content will be turned into a key-based array, and accessible like this:

@app.route("/users", methods=['POST'])
def create_user():
	json_data = request.get_json()
  # json_data = { "username": "...", "firstname": "...", "lastname": "..."}

Deserializing JSON

Just as serializing a class instance to send it as an API response, retrieving the request body to store it in an object is a good practice!

And thanks to the marshmallow library, we'll be able to store the request content in an object (and validate the data) very simply, without installing anything more:

# description: adds a user to the list
@app.route("/users", methods=['POST'])
def create_user():
    json_data = request.get_json()
    try:
        user = user_validator.load(json_data)
        if user:
            users.append(user)
            return "User created", 201
    except Exception as err:
        return err.__str__(), 400

Thanks to our user_validator, an exception will be thrown if the object received as input doesn't match the expected schema (and will return a 400 error)!

Creating a dynamic route

To define a route that will take one or more parameters in its URL, you can define each parameter in the form <type:name>, like this:

# description : Returns the user matching the id
@app.route("/users/<int:id>")
def get_user_by_id(id: int) :
	user = users[id]
	return user_validator.dump(user)

Try it yourself: http://127.0.0.1:5000/users/0

Note that we could also have passed a string, such as a username, like this: /users/<String:username>

Bonus: creating a DTO

If you're not familiar with the concept of Data Transfer Object, you can read our article on the subject: https://code-garage.fr/blog/what-are-data-transfer-objects-dto-used-for/

In short, a DTO is a validator that will only be used to validate the content of an object that travels over the network (here, the API response). It's exactly the same validation concept as before, except that we'll have a class dedicated to this response:

class GetUsersResponse:
	def __init__(self, users):
		self.users = users
		self.count = len(users)

class GetUsersResponseDTO(Schema):
	users = fields.List(fields.Nested(UserSchema()))
	count = fields.Integer()
	
get_users_response_validator = GetUsersResponseDTO()

# description: returns the list of all users
@app.route("/users")
def get_users() :
	response = GetUsersResponse(users);
	return get_users_response_validator.dump(response)

This also lets you see how to validate a list of objects with Marshmallow, using fields.List(fields.Nested(UserSchema()))!

The complete code

Find the complete code for this tutorial below:

from flask import Flask, request
from marshmallow import Schema, fields

app = Flask(__name__)

### Classes

class User:
	username: str
	firstname: str
	lastname: str
	password: str
	def __init__(self, username: str, firstname: str, lastname: str):
		self.username = username
		self.firstname = firstname
		self.lastname = lastname
		self.password = "azejbnzdfunIJIN65" #randomly generated

class UserSchema(Schema):
    username = fields.Str(required=True)
    firstname = fields.Str(required=True)
    lastname = fields.Str(required=True)

# We create the validator from our schema
user_validator = UserSchema()

### Data

users = []
users.append(User("JohnDoe", "John", "Doe"))

### Examples

@app.route("/")
def hello_world():
    return "Hello, World!"

@app.route("/json")
def hello_json():
    return {"str1": "Hello", "str2": "world"}
	# or return ["hello", "world"]

@app.route("/hello") # Example /hello?name=John
def hello_name():
    name = request.args.get("name")
    return "Hello, " + name

### Routes

# description: returns the first user in the list
@app.route("/user")
def get_user():
    first_user = users[0]
		# dump(...) allows converting a class instance to json
    response = user_validator.dump(first_user)
    return response

# description: adds a user to the list
@app.route("/users", methods=['POST'])
def create_user():
    json_data = request.get_json()
    try:
        user = user_validator.load(json_data)
        if user:
            users.append(user)
            return "User created", 201
    except Exception as err:
        return err.__str__(), 400

# description : Returns the user matching the id
@app.route("/users/<int:id>")
def get_user_by_id(id: int) :
	user = users[id]
	return user_validator.dump(user)

class GetUsersResponse:
	def __init__(self, users):
		self.users = users
		self.count = len(users)

class GetUsersResponseDTO(Schema):
	users = fields.List(fields.Nested(UserSchema()))
	count = fields.Integer()
	
get_users_response_validator = GetUsersResponseDTO()

# description: returns the list of all users
@app.route("/users")
def get_users() :
	response = GetUsersResponse(users);
	return get_users_response_validator.dump(response)

Finished reading this article?
Our complete courses
Take it to the next level with our courses!

Complete courses, exercises and certificates to really learn programming!

4.8 average rating

Comments (0)

to leave a comment

No comments yet