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:
GETto fetch dataPOSTto create dataPUTorPATCHto update dataDELETEto 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:
200for success201for created400for bad request401for unauthorized404for not found500for 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.