Flask RESTful API — Core Concepts
REST in Flask: two approaches
You can build REST APIs in Flask two ways: plain Flask with jsonify, or Flask-RESTful which adds resource classes and request parsing. Both are valid. Plain Flask is simpler for small APIs; Flask-RESTful adds structure as APIs grow.
Plain Flask
@app.route('/api/users', methods=['GET'])
def list_users():
users = User.query.all()
return jsonify([{'id': u.id, 'name': u.name} for u in users])
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
user = User(name=data['name'], email=data['email'])
db.session.add(user)
db.session.commit()
return jsonify({'id': user.id, 'name': user.name}), 201
Flask-RESTful
from flask_restful import Api, Resource, reqparse
api = Api(app)
class UserListResource(Resource):
def get(self):
users = User.query.all()
return [{'id': u.id, 'name': u.name} for u in users]
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('name', required=True)
parser.add_argument('email', required=True)
args = parser.parse()
user = User(name=args['name'], email=args['email'])
db.session.add(user)
db.session.commit()
return {'id': user.id, 'name': user.name}, 201
api.add_resource(UserListResource, '/api/users')
HTTP methods and their meaning
REST uses HTTP methods as verbs:
| Method | Purpose | Idempotent | Example |
|---|---|---|---|
| GET | Read data | Yes | Get user profile |
| POST | Create new | No | Create new user |
| PUT | Replace entirely | Yes | Update all user fields |
| PATCH | Partial update | No* | Update just the email |
| DELETE | Remove | Yes | Delete a user |
Idempotent means calling it multiple times has the same effect as calling it once. DELETE /users/42 twice still results in user 42 being gone. POST /users twice creates two users.
Status codes that matter
Your API communicates through status codes:
- 200 — OK (successful GET, PUT, PATCH)
- 201 — Created (successful POST)
- 204 — No Content (successful DELETE with no response body)
- 400 — Bad Request (invalid input data)
- 401 — Unauthorized (not logged in)
- 403 — Forbidden (logged in but not allowed)
- 404 — Not Found (resource doesn’t exist)
- 409 — Conflict (duplicate email, etc.)
- 422 — Unprocessable Entity (valid JSON but fails validation)
- 500 — Server Error (your bug)
Using correct codes helps clients handle responses programmatically instead of parsing error messages.
Request parsing and validation
Never trust client data. Validate everything:
@app.route('/api/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data:
return jsonify({'error': 'Request body must be JSON'}), 400
errors = {}
if not data.get('name'):
errors['name'] = 'Name is required'
if not data.get('email'):
errors['email'] = 'Email is required'
elif not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
errors['email'] = 'Invalid email format'
if errors:
return jsonify({'errors': errors}), 422
# Safe to proceed
For larger APIs, use Marshmallow or Pydantic for schema-based validation instead of manual checks.
Response formatting
Consistent response structure makes APIs easier to consume:
# Success
{
"data": {"id": 42, "name": "Alice"},
"meta": {"request_id": "abc123"}
}
# Error
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {"email": "Already registered"}
}
}
# List with pagination
{
"data": [{"id": 1}, {"id": 2}],
"meta": {
"page": 1,
"per_page": 20,
"total": 87,
"pages": 5
}
}
Pick a structure and stick with it across every endpoint. Inconsistency is the biggest frustration for API consumers.
Error handling
Register global error handlers to catch unhandled exceptions:
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': {'code': 'NOT_FOUND', 'message': 'Resource not found'}}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({'error': {'code': 'SERVER_ERROR', 'message': 'Internal server error'}}), 500
@app.errorhandler(ValidationError)
def validation_error(e):
return jsonify({'error': {'code': 'VALIDATION_ERROR', 'message': str(e)}}), 422
Without these, Flask returns HTML error pages — confusing for API clients expecting JSON.
Common misconception
“REST means you must use Flask-RESTful.” The library Flask-RESTful is optional. REST is an architectural style, not a library. Plain Flask with jsonify, proper HTTP methods, and consistent URL patterns is a perfectly valid REST API. Flask-RESTful adds convenience (resource classes, request parsing), but it’s not required and some teams prefer plain Flask with Marshmallow for its flexibility.
Authentication for APIs
APIs typically use tokens instead of session cookies:
from functools import wraps
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'Token required'}), 401
user = User.verify_token(token)
if not user:
return jsonify({'error': 'Invalid token'}), 401
return f(user, *args, **kwargs)
return decorated
@app.route('/api/profile')
@token_required
def profile(user):
return jsonify({'id': user.id, 'name': user.name})
One thing to remember: A good REST API is predictable. Consistent URLs, correct status codes, validated input, and structured responses. The code that generates data matters less than the contract you expose to consumers.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.