How to Create an API with Flask

How to Create an API with Flask

Building an API with Flask is one of those projects that feels simple at first and then quietly teaches you a lot about web development, architecture, and clean code. Flask is small, flexible, and friendly. It does not force a heavy structure on you, which is great when you want to move fast, learn the basics, or build something exactly the way you want.

That flexibility is also why Flask is such a good choice for APIs. You can start with a tiny endpoint in a single file, and later grow it into a proper application with authentication, database support, validation, error handling, and tests. The journey feels natural. You are not fighting the framework; you are shaping it to fit your project.

In this article, we will build an API step by step with Flask. We will begin with a very small example, then expand into a real CRUD API with a database, input validation, proper responses, and a few production-minded improvements. Along the way, you will see code examples you can reuse in your own projects.


What is an API?

API stands for Application Programming Interface. In simple terms, an API is a way for one software application to talk to another.

When people talk about building an API with Flask, they usually mean building a web service that returns JSON data instead of rendering HTML pages. For example:

  • a mobile app might call your API to get user data

  • a frontend app built with React or Vue might use your API to load products

  • another backend system might send or receive data through your API

A Flask API usually listens for HTTP requests such as:

  • GET to fetch data

  • POST to create data

  • PUT or PATCH to update data

  • DELETE to remove data

And it returns JSON responses like:

{
  "message": "Success",
  "data": {
    "id": 1,
    "name": "Flask API"
  }
}

Why Flask for APIs?

Flask is a great choice for API development because it is:

Lightweight

Flask does not come with too many rules. You can build only what you need.

Easy to learn

The learning curve is gentle, especially if you already understand Python.

Flexible

You can organize your project however you want. That is useful for small tools and large systems alike.

Good for prototyping

If you have an idea and want to build it fast, Flask helps you move quickly.

Powerful enough for production

Flask may be simple, but it is used in real-world applications all the time.


What we will build

We will create a small API for managing books. This is a nice example because it is easy to understand and still includes the most important CRUD operations.

Our API will support:

  • listing books

  • getting one book by ID

  • creating a book

  • updating a book

  • deleting a book

Later, we will improve it with:

  • validation

  • database integration

  • error handling

  • pagination

  • basic authentication

  • testing


Project setup

First, create a virtual environment and install Flask.

python -m venv venv

Activate it:

On Windows:

venv\Scripts\activate

On macOS or Linux:

source venv/bin/activate

Now install Flask:

pip install flask

If you want to work with a database and validation later, you can also install:

pip install flask-sqlalchemy flask-marshmallow marshmallow flask-migrate

For now, we will start small.


A very simple Flask API

Create a file called app.py:

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/api/hello", methods=["GET"])
def hello():
    return jsonify({
        "message": "Hello, world!"
    })

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

Run it:

python app.py

Now open your browser or use a tool like Postman and visit:

http://127.0.0.1:5000/api/hello

You should see:

{
  "message": "Hello, world!"
}

That is your first Flask API endpoint. It is tiny, but it proves the idea: Flask receives a request and returns JSON.


Understanding the core parts

Let’s break down the important pieces.

Flask(__name__)

This creates the application instance.

@app.route(...)

This tells Flask what URL to listen to.

methods=["GET"]

This defines which HTTP methods are allowed.

jsonify(...)

This converts Python dictionaries into proper JSON responses.


Building a basic in-memory CRUD API

Now let’s build something more realistic. We will store books in memory first. This is not permanent storage, but it is a helpful step before adding a database.

Here is a full example:

from flask import Flask, jsonify, request

app = Flask(__name__)

books = [
    {"id": 1, "title": "The Alchemist", "author": "Paulo Coelho"},
    {"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"},
]

def find_book(book_id):
    return next((book for book in books if book["id"] == book_id), None)

@app.route("/api/books", methods=["GET"])
def get_books():
    return jsonify({
        "message": "Books retrieved successfully",
        "data": books
    })

@app.route("/api/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
    book = find_book(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    return jsonify({
        "message": "Book retrieved successfully",
        "data": book
    })

@app.route("/api/books", methods=["POST"])
def create_book():
    data = request.get_json()

    if not data:
        return jsonify({"error": "Invalid JSON body"}), 400

    title = data.get("title")
    author = data.get("author")

    if not title or not author:
        return jsonify({"error": "Title and author are required"}), 400

    new_book = {
        "id": books[-1]["id"] + 1 if books else 1,
        "title": title,
        "author": author
    }

    books.append(new_book)

    return jsonify({
        "message": "Book created successfully",
        "data": new_book
    }), 201

@app.route("/api/books/<int:book_id>", methods=["PUT"])
def update_book(book_id):
    book = find_book(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    data = request.get_json()
    if not data:
        return jsonify({"error": "Invalid JSON body"}), 400

    book["title"] = data.get("title", book["title"])
    book["author"] = data.get("author", book["author"])

    return jsonify({
        "message": "Book updated successfully",
        "data": book
    })

@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
    book = find_book(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    books.remove(book)

    return jsonify({
        "message": "Book deleted successfully"
    })

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

This gives you a full CRUD API in one file. It is not the final version, but it is a great foundation.


Testing the endpoints

You can test them with curl.

Get all books

curl http://127.0.0.1:5000/api/books

Get one book

curl http://127.0.0.1:5000/api/books/1

Create a book

curl -X POST http://127.0.0.1:5000/api/books \
  -H "Content-Type: application/json" \
  -d '{"title":"Clean Code","author":"Robert C. Martin"}'

Update a book

curl -X PUT http://127.0.0.1:5000/api/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Clean Code Updated"}'

Delete a book

curl -X DELETE http://127.0.0.1:5000/api/books/1

When you test your API this way, you start thinking like both a developer and a user of the API. That mindset is very important.


Why in-memory storage is not enough

The in-memory list is useful for learning, but there is a problem: every time the app restarts, all data disappears. That is fine for examples, but not okay for real projects.

For a real API, you need a database.

Flask works very nicely with SQLite, PostgreSQL, MySQL, and others. For a beginner-friendly setup, SQLite is a great place to begin because it requires very little configuration.


Adding a database with Flask-SQLAlchemy

Install the needed packages:

pip install flask-sqlalchemy flask-migrate

Now let’s build a cleaner project structure.

Suggested folder structure

project/
│
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── extensions.py
│   └── config.py
│
├── migrations/
├── instance/
├── app.py
└── requirements.txt

This structure helps keep your code organized as the project grows.


Application factory pattern

Instead of placing everything in one file, Flask encourages a more scalable style called the application factory pattern.

app/extensions.py

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

app/config.py

import os

class Config:
    SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///books.db")
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")

app/__init__.py

from flask import Flask
from .config import Config
from .extensions import db, migrate

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    migrate.init_app(app, db)

    from .routes import api
    app.register_blueprint(api, url_prefix="/api")

    return app

app.py

from app import create_app

app = create_app()

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

This setup is cleaner and easier to maintain.


Creating the model

Now define the book model.

app/models.py

from .extensions import db

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    author = db.Column(db.String(150), nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())

    def to_dict(self):
        return {
            "id": self.id,
            "title": self.title,
            "author": self.author,
            "created_at": self.created_at.isoformat() if self.created_at else None
        }

A small to_dict() method is very handy because it gives you a clean JSON-friendly version of the object.


Creating the database

Open the Python shell or use Flask CLI commands.

First, set the environment variable for Flask. On Windows PowerShell:

$env:FLASK_APP="app.py"

On macOS/Linux:

export FLASK_APP=app.py

Now initialize migrations:

flask db init
flask db migrate -m "Create book table"
flask db upgrade

This creates your database tables.


Building API routes with Blueprint

Using Blueprints keeps your API organized.

app/routes.py

from flask import Blueprint, jsonify, request
from .extensions import db
from .models import Book

api = Blueprint("api", __name__)

@api.route("/books", methods=["GET"])
def get_books():
    books = Book.query.order_by(Book.id.desc()).all()
    return jsonify({
        "message": "Books retrieved successfully",
        "data": [book.to_dict() for book in books]
    })

@api.route("/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    return jsonify({
        "message": "Book retrieved successfully",
        "data": book.to_dict()
    })

@api.route("/books", methods=["POST"])
def create_book():
    data = request.get_json()

    if not data:
        return jsonify({"error": "Invalid JSON body"}), 400

    title = data.get("title")
    author = data.get("author")

    if not title or not author:
        return jsonify({"error": "Title and author are required"}), 400

    book = Book(title=title, author=author)
    db.session.add(book)
    db.session.commit()

    return jsonify({
        "message": "Book created successfully",
        "data": book.to_dict()
    }), 201

@api.route("/books/<int:book_id>", methods=["PUT"])
def update_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    data = request.get_json()
    if not data:
        return jsonify({"error": "Invalid JSON body"}), 400

    title = data.get("title")
    author = data.get("author")

    if title is not None:
        book.title = title
    if author is not None:
        book.author = author

    db.session.commit()

    return jsonify({
        "message": "Book updated successfully",
        "data": book.to_dict()
    })

@api.route("/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    db.session.delete(book)
    db.session.commit()

    return jsonify({
        "message": "Book deleted successfully"
    })

Now your API is backed by a database instead of memory.


Better JSON response format

A clean API usually returns consistent responses. That makes frontend integration easier and debugging less painful.

A common structure is:

{
  "message": "Success",
  "data": {...},
  "errors": null
}

Or for an error:

{
  "message": "Validation failed",
  "errors": {
    "title": ["Title is required"]
  }
}

Consistency matters. It may look small when you are building the first endpoint, but later you will appreciate it deeply.


Adding validation

Right now, our code checks input manually. That works, but as the API grows, validation can become repetitive.

A nicer approach is to use Marshmallow.

Install it:

pip install marshmallow flask-marshmallow

Create a schema:

app/schemas.py

from flask_marshmallow import Marshmallow

ma = Marshmallow()

class BookSchema(ma.Schema):
    class Meta:
        fields = ("id", "title", "author", "created_at")

book_schema = BookSchema()
books_schema = BookSchema(many=True)

Initialize Marshmallow in extensions.py or __init__.py:

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow

db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()

Then in create_app():

from .extensions import db, migrate, ma

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    migrate.init_app(app, db)
    ma.init_app(app)

    from .routes import api
    app.register_blueprint(api, url_prefix="/api")

    return app

Now you can use schemas in your routes to serialize data.


Handling errors properly

A good API should fail gracefully.

Instead of returning confusing responses or crashing, return clear messages.

Example: custom 404 error response

from flask import jsonify

def register_error_handlers(app):
    @app.errorhandler(404)
    def not_found(error):
        return jsonify({
            "message": "Resource not found"
        }), 404

    @app.errorhandler(500)
    def internal_server_error(error):
        return jsonify({
            "message": "Internal server error"
        }), 500

Then call register_error_handlers(app) inside create_app().

That way, users of your API get a proper JSON response even when something goes wrong.


Pagination

If your API returns many records, pagination is a must.

Here is a simple example:

@api.route("/books", methods=["GET"])
def get_books():
    page = request.args.get("page", 1, type=int)
    per_page = request.args.get("per_page", 10, type=int)

    pagination = Book.query.paginate(page=page, per_page=per_page, error_out=False)
    books = pagination.items

    return jsonify({
        "message": "Books retrieved successfully",
        "data": [book.to_dict() for book in books],
        "pagination": {
            "page": pagination.page,
            "per_page": pagination.per_page,
            "total_pages": pagination.pages,
            "total_items": pagination.total
        }
    })

Now your API can handle larger datasets without overwhelming the client.


Filtering and searching

A real API often needs search features. For example, you may want to search books by title.

@api.route("/books/search", methods=["GET"])
def search_books():
    query = request.args.get("q", "")

    books = Book.query.filter(Book.title.ilike(f"%{query}%")).all()

    return jsonify({
        "message": "Search results",
        "data": [book.to_dict() for book in books]
    })

This is the kind of small feature that makes an API feel much more useful in practice.


Adding authentication

Not every API needs authentication at the start, but many real ones do. A common approach is token-based authentication, often using JSON Web Tokens, also known as JWT.

Install:

pip install flask-jwt-extended

Initialize it:

from flask_jwt_extended import JWTManager

jwt = JWTManager()

Inside create_app():

from .extensions import db, migrate, ma, jwt

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)
    app.config["JWT_SECRET_KEY"] = "super-secret-key"

    db.init_app(app)
    migrate.init_app(app, db)
    ma.init_app(app)
    jwt.init_app(app)

    from .routes import api
    app.register_blueprint(api, url_prefix="/api")

    return app

A simple login route might look like this:

from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity

@api.route("/login", methods=["POST"])
def login():
    data = request.get_json()

    if data.get("username") == "admin" and data.get("password") == "password":
        token = create_access_token(identity="admin")
        return jsonify({"access_token": token})

    return jsonify({"error": "Invalid credentials"}), 401

@api.route("/protected", methods=["GET"])
@jwt_required()
def protected():
    current_user = get_jwt_identity()
    return jsonify({"message": f"Hello, {current_user}!"})

This is a simple example, but it shows the idea clearly.


A cleaner version of the API

At some point, your API should feel less like a script and more like a real application. Here is a cleaner version of the key pieces together.

app/models.py

from .extensions import db

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    author = db.Column(db.String(150), nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())

    def to_dict(self):
        return {
            "id": self.id,
            "title": self.title,
            "author": self.author,
            "created_at": self.created_at.isoformat() if self.created_at else None
        }

app/routes.py

from flask import Blueprint, jsonify, request
from .extensions import db
from .models import Book

api = Blueprint("api", __name__)

@api.route("/books", methods=["GET"])
def get_books():
    books = Book.query.all()
    return jsonify([book.to_dict() for book in books])

@api.route("/books", methods=["POST"])
def create_book():
    data = request.get_json()

    if not data or not data.get("title") or not data.get("author"):
        return jsonify({"error": "Title and author are required"}), 400

    book = Book(title=data["title"], author=data["author"])
    db.session.add(book)
    db.session.commit()

    return jsonify(book.to_dict()), 201

This version is simple, readable, and easy to extend.


Testing your Flask API

Testing is one of those things people often delay, and then later wish they had started earlier. A few API tests can save hours of debugging.

Install pytest:

pip install pytest

Create a test file:

tests/test_books.py

import pytest
from app import create_app
from app.extensions import db
from app.models import Book

@pytest.fixture
def app():
    app = create_app()
    app.config["TESTING"] = True
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"

    with app.app_context():
        db.create_all()
        yield app
        db.drop_all()

@pytest.fixture
def client(app):
    return app.test_client()

def test_get_books(client):
    response = client.get("/api/books")
    assert response.status_code == 200

You can add more tests for creating, updating, and deleting books.

Testing helps you trust your API. That trust matters.


A few best practices for Flask APIs

Here are some habits that make a big difference over time.

Keep routes small

Do not put too much logic inside a single route. Move business logic into services when needed.

Return consistent responses

Make your success and error responses follow a pattern.

Validate input

Never assume the incoming request is correct.

Use proper HTTP status codes

Examples:

  • 200 for success

  • 201 for created

  • 400 for bad request

  • 401 for unauthorized

  • 404 for not found

  • 500 for server errors

Separate concerns

Models, routes, validation, and configuration should each have their own place.

Add tests early

Even a few tests are better than none.

Document your API

A README or OpenAPI/Swagger documentation makes life much easier for others.


A sample README for your API

Here is a simple README snippet:

# Books API

A Flask REST API for managing books.

## Features
- Create, read, update, and delete books
- JSON responses
- Database support with SQLite
- Basic validation

## Setup

```bash
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
flask db upgrade
python app.py

Endpoints

  • GET /api/books

  • GET /api/books/

  • POST /api/books

  • PUT /api/books/

  • DELETE /api/books/

Good documentation makes your API feel alive and usable.

---

## Deploying a Flask API

Once your API works locally, you may want to deploy it.

Common deployment options include:

- Render
- Railway
- Fly.io
- Heroku-style platforms
- VPS servers with Gunicorn and Nginx

For production, you should not use Flask’s built-in development server. Instead, use a production WSGI server like Gunicorn.

Install Gunicorn:

```bash
pip install gunicorn

Run:

gunicorn app:app

For a proper deployment, remember:

  • set environment variables

  • disable debug mode

  • use a production database

  • secure your secret keys

  • configure logging

These details are not glamorous, but they matter a lot.


Example of environment variables

Instead of hardcoding secrets, use environment variables.

.env

FLASK_APP=app.py
FLASK_ENV=production
SECRET_KEY=your-secret-key
DATABASE_URL=sqlite:///books.db
JWT_SECRET_KEY=another-secret-key

You can load them using python-dotenv:

pip install python-dotenv

Then Flask can read them safely during development.


Common mistakes when building a Flask API

It is completely normal to make mistakes early on. Most developers do. The important thing is learning the patterns behind them.

1. Keeping everything in one file

It works for demos, but it becomes painful fast.

2. Forgetting to validate JSON input

This leads to strange bugs and poor error messages.

3. Not using status codes correctly

A 200 for everything makes an API harder to understand.

4. Skipping tests

Then one small change breaks three routes and you spend an evening debugging.

5. Hardcoding secrets

That is risky and unnecessary.

6. Returning inconsistent JSON

The frontend then has to guess what shape the response takes.


A more human way to think about APIs

When people first learn APIs, they often focus on code syntax alone. But an API is really a conversation.

A client asks: “Can I get the book list?”

Your API replies: “Yes, here it is.”

The client says: “Can I add a new book?”

Your API replies: “Yes, but please send a title and author.”

The client says: “I forgot the title.”

Your API replies: “That is okay, but I need it before I can continue.”

That is what good API design feels like. Clear. Predictable. Respectful of the person or system on the other side.

That human feeling is worth protecting.


Full example of a small Flask API project

Here is a compact project version you can build from.

app.py

from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///books.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    author = db.Column(db.String(150), nullable=False)

    def to_dict(self):
        return {
            "id": self.id,
            "title": self.title,
            "author": self.author
        }

@app.route("/api/books", methods=["GET"])
def get_books():
    books = Book.query.all()
    return jsonify([book.to_dict() for book in books])

@app.route("/api/books", methods=["POST"])
def create_book():
    data = request.get_json()

    if not data or not data.get("title") or not data.get("author"):
        return jsonify({"error": "Title and author are required"}), 400

    book = Book(title=data["title"], author=data["author"])
    db.session.add(book)
    db.session.commit()

    return jsonify(book.to_dict()), 201

@app.route("/api/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    return jsonify(book.to_dict())

@app.route("/api/books/<int:book_id>", methods=["PUT"])
def update_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    data = request.get_json()
    if not data:
        return jsonify({"error": "Invalid JSON body"}), 400

    book.title = data.get("title", book.title)
    book.author = data.get("author", book.author)

    db.session.commit()

    return jsonify(book.to_dict())

@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
    book = Book.query.get(book_id)
    if not book:
        return jsonify({"error": "Book not found"}), 404

    db.session.delete(book)
    db.session.commit()

    return jsonify({"message": "Book deleted successfully"})

if __name__ == "__main__":
    with app.app_context():
        db.create_all()
    app.run(debug=True)

This is not the most advanced structure, but it is useful as a learning project or a quick prototype.


Where to go next

Once you are comfortable with the basics, you can improve your Flask API by adding:

  • authentication with JWT

  • role-based access control

  • better validation with Marshmallow or Pydantic

  • request logging

  • rate limiting

  • API versioning

  • Swagger/OpenAPI documentation

  • background jobs with Celery or RQ

  • file uploads

  • email notifications

  • better error tracking

At that point, your Flask API becomes a real product, not just a tutorial project.


Final thoughts

Creating an API with Flask is a very rewarding path. It starts with a few lines of code and grows into a full backend system that can power websites, mobile apps, automation tools, and internal dashboards.

The best part is that Flask lets you learn gradually. You do not need to understand everything before you begin. You can start with one route, one response, one idea. Then you keep improving it piece by piece.

That is often how real software is built anyway: not all at once, but carefully, with patience, curiosity, and a few small wins along the way.

If you build your Flask API with clear routes, proper validation, good error handling, and a sensible structure, you will end up with something that feels reliable and easy to extend. And that feeling, honestly, is one of the nicest parts of backend development.