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.
First Endpoint: Build It and Run It
Here’s a barebones setup that gets you a working API.
Install dependencies:
python -m venv .venvsource .venv/bin/activate
pip install Flask flask-sqlalchemy flask-migrate marshmallow flask-marshmallow flask-cors
app.py:
from flask import Flask, request, jsonify, abortfrom flask_sqlalchemy import SQLAlchemyfrom flask_migrate import Migratefrom marshmallow import Schema, fields, ValidationErrorfrom 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)
# Modelclass 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="")
# Schemaclass 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.
Organizing the Project
A single file won’t cut it once your API grows. A cleaner setup looks like this:
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:
{ "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.
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
:
FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install -r requirements.txtCOPY . .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
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!
