See all posts

How to Create a RESTful API Using Flask

How to Create a RESTful API Using Flask

When people talk about Flask, they usually mention how lightweight it is, you can spin up a working app with just a few lines of Python. That’s true, but once you start building a RESTful API, the real work begins: organizing your project, validating input, handling errors, testing, and thinking ahead to deployment. This walkthrough covers those parts with code you can actually run.

Why Flask Works Well for APIs

Flask doesn’t force you into a particular project structure. You get routing, request handling, and extensions for the rest. That makes it ideal if you want flexibility or if you’re integrating Python-heavy workloads (data science, ML, analytics).

The trade-off is that you’re responsible for choosing tools for things like validation, database access, and authentication. Some developers love that flexibility; others find it overwhelming. If you want an async-first framework that generates docs for you, FastAPI is worth checking out. But for many CRUD-heavy projects where Python integration matters, Flask still does the job well.

flask-flexbility

First Endpoint: Build It and Run It

Here’s a barebones setup that gets you a working API.

Install dependencies:

bash
python -m venv .venv
source .venv/bin/activate
bash
pip install Flask flask-sqlalchemy flask-migrate marshmallow flask-marshmallow flask-cors

app.py:

py
from flask import Flask, request, jsonify, abort
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from marshmallow import Schema, fields, ValidationError
from flask_cors import CORS
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
CORS(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# Model
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
description = db.Column(db.String(500), default="")
# Schema
class ItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
description = fields.Str()
item_schema = ItemSchema()
items_schema = ItemSchema(many=True)
# Routes
@app.route("/items", methods=["GET"])
def list_items():
return jsonify(items_schema.dump(Item.query.all()))
@app.route("/items", methods=["POST"])
def create_item():
try:
data = item_schema.load(request.json)
except ValidationError as err:
return jsonify({"errors": err.messages}), 400
item = Item(**data)
db.session.add(item)
db.session.commit()
return item_schema.jsonify(item), 201
@app.route("/items/<int:item_id>", methods=["GET"])
def get_item(item_id):
item = Item.query.get_or_404(item_id)
return item_schema.jsonify(item)

Run it with flask run. You now have endpoints for:

  • GET /items

  • POST /items

  • GET /items/<id>

It’s not fancy, but it’s enough to see how things connect.

Flask first API endpoint example with client-server request and response

Organizing the Project

A single file won’t cut it once your API grows. A cleaner setup looks like this:

bash
myapi/
├─ app/
│ ├─ __init__.py # create_app factory
│ ├─ config.py
│ ├─ models.py
│ ├─ schemas.py
│ ├─ routes/
│ │ ├─ items.py
│ ├─ services/
│ │ └─ item_service.py
│ └─ errors.py
├─ migrations/
├─ tests/
└─ manage.py

Models, schemas, and routes live in their own spots. Business logic goes in services/. It feels like overkill at first, but the benefits show up fast when your API has 20+ endpoints.

Pieces You Don’t Want to Skip

Validation

Use Marshmallow schemas to reject bad data. Validate on input, serialize on output.

Error Handling

Return JSON consistently. For example:

py
{
"errors": {"name": ["Missing data for required field."]}
}

Pagination & Filtering

Never return thousands of rows at once. Paginate with safe defaults (per_page=20) and add simple filters.

Authentication

JWT tokens are common. For bigger projects, use an identity provider like Auth0 or Okta and just validate tokens in Flask.

Security

  • Limit origins with flask-cors in production.

  • Add rate limiting with flask-limiter.

  • Don’t run the Flask dev server in production. Use Gunicorn or uWSGI.

  • Apply security headers with Flask-Talisman.

Testing the API

pytest works well with Flask’s test client.

py
def test_create_item(client):
res = client.post("/items", json={"name": "Coffee"})
assert res.status_code == 201
data = res.get_json()
assert data["name"] == "Coffee"

Tests like this stop regressions before they hit production.

Docker and Deployment

Here’s a minimal Dockerfile:

dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "app:create_app()", "-w", "4", "-b", "0.0.0.0:8000"]

This runs the app under Gunicorn with 4 workers. You can drop it onto ECS, Cloud Run, or any container-friendly platform.

Quick Checklist Before You Ship

  • Input validation

  • Consistent error responses

  • Correct HTTP status codes

  • Authentication + rate limiting

  • Logging and monitoring hooks

  • Automated tests

  • Docker setup + proper WSGI server

Pre-deployment checklist for Flask REST API projects

Conclusion

Flask is small on purpose, but with the right pieces, validation, error handling, tests, and deployment setup, it scales into something reliable. Start with the basics above, then layer in the practices that make sense for your project.


Windframe is an AI visual editor for rapidly building stunning web UIs & websites

Start building stunning web UIs & websites!

Build from scratch or select prebuilt tailwind templates