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:

MethodPurposeIdempotentExample
GETRead dataYesGet user profile
POSTCreate newNoCreate new user
PUTReplace entirelyYesUpdate all user fields
PATCHPartial updateNo*Update just the email
DELETERemoveYesDelete 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.

pythonflaskapirest

See Also