Build REST API in Python with Flask: Complete Guide

Learn to build robust REST APIs with Flask from scratch. Complete tutorial covering setup, CRUD operations, authentication, and best practices.

How to Build a REST API in Python with Flask: Complete Step-by-Step Guide

Building robust REST APIs is a fundamental skill for modern web development. Flask, Python's lightweight and flexible web framework, provides an excellent foundation for creating scalable APIs. This comprehensive tutorial will guide you through building a complete REST API from scratch, covering everything from basic setup to advanced authentication and CRUD operations.

What is Flask and Why Choose It for REST APIs?

Flask is a micro web framework written in Python that's designed to make getting started with web development quick and easy, with the ability to scale up to complex applications. Unlike heavyweight frameworks, Flask gives you the flexibility to choose your components and structure your application as needed.

Key advantages of Flask for API development: - Lightweight and minimalist - Highly customizable and extensible - Excellent documentation and community support - Built-in development server and debugger - RESTful request dispatching - Secure cookie support for client-side sessions

Prerequisites and Environment Setup

Before diving into development, ensure you have the following prerequisites:

System Requirements: - Python 3.7 or higher - pip (Python package installer) - Text editor or IDE (VS Code, PyCharm recommended) - Basic understanding of HTTP methods and REST principles

Setting up your development environment:

`bash

Create a new directory for your project

mkdir flask-rest-api cd flask-rest-api

Create a virtual environment

python -m venv venv

Activate the virtual environment

On Windows:

venv\Scripts\activate

On macOS/Linux:

source venv/bin/activate

Install required packages

pip install Flask Flask-SQLAlchemy Flask-JWT-Extended python-dotenv `

Create a requirements.txt file to track dependencies:

`txt Flask==2.3.3 Flask-SQLAlchemy==3.0.5 Flask-JWT-Extended==4.5.2 python-dotenv==1.0.0 Werkzeug==2.3.7 `

Project Structure and Organization

Organizing your Flask application properly from the start ensures maintainability and scalability. Here's the recommended project structure:

` flask-rest-api/ ├── app/ │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ └── user.py │ ├── routes/ │ │ ├── __init__.py │ │ ├── auth.py │ │ └── users.py │ └── utils/ │ ├── __init__.py │ └── helpers.py ├── config.py ├── run.py ├── .env └── requirements.txt `

Creating Your First Flask Application

Let's start by creating the basic Flask application structure:

config.py: `python import os from datetime import timedelta

class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'your-secret-key-here' SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db' SQLALCHEMY_TRACK_MODIFICATIONS = False JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-string' JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) `

app/__init__.py: `python from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager from config import Config

Initialize extensions

db = SQLAlchemy() jwt = JWTManager()

def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) # Initialize extensions with app db.init_app(app) jwt.init_app(app) # Register blueprints from app.routes.auth import auth_bp from app.routes.users import users_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(users_bp, url_prefix='/api/users') return app `

run.py: `python from app import create_app, db from dotenv import load_dotenv

load_dotenv()

app = create_app()

@app.before_first_request def create_tables(): db.create_all()

if __name__ == '__main__': app.run(debug=True) `

Understanding REST Principles and HTTP Methods

REST (Representational State Transfer) is an architectural style that defines a set of constraints for creating web services. A RESTful API uses standard HTTP methods to perform operations on resources.

Core HTTP Methods: - GET: Retrieve data from the server - POST: Create new resources - PUT: Update existing resources (complete replacement) - PATCH: Partial update of resources - DELETE: Remove resources

REST Best Practices: - Use nouns for resource names (not verbs) - Use HTTP status codes appropriately - Implement consistent URL patterns - Version your API - Use JSON for data exchange

Building Your First Route

Let's create a simple "Hello World" route to test our setup:

app/routes/__init__.py: `python from flask import Blueprint

This file can remain empty or contain shared route utilities

`

app/routes/users.py: `python from flask import Blueprint, jsonify, request from app import db from app.models.user import User from flask_jwt_extended import jwt_required, get_jwt_identity

users_bp = Blueprint('users', __name__)

@users_bp.route('/hello', methods=['GET']) def hello_world(): return jsonify({ 'message': 'Hello, World!', 'status': 'success' }), 200

@users_bp.route('/', methods=['GET']) def get_all_users(): users = User.query.all() return jsonify({ 'users': [user.to_dict() for user in users], 'count': len(users) }), 200 `

Database Models with SQLAlchemy

SQLAlchemy is Flask's recommended ORM (Object-Relational Mapping) tool. Let's create our User model:

app/models/user.py: `python from app import db from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime

class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) first_name = db.Column(db.String(50), nullable=True) last_name = db.Column(db.String(50), nullable=True) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def __init__(self, username, email, password, first_name=None, last_name=None): self.username = username self.email = email self.set_password(password) self.first_name = first_name self.last_name = last_name def set_password(self, password): """Hash and set the user's password""" self.password_hash = generate_password_hash(password) def check_password(self, password): """Check if the provided password matches the hash""" return check_password_hash(self.password_hash, password) def to_dict(self): """Convert user object to dictionary""" return { 'id': self.id, 'username': self.username, 'email': self.email, 'first_name': self.first_name, 'last_name': self.last_name, 'is_active': self.is_active, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } def __repr__(self): return f'' `

Implementing CRUD Operations

CRUD operations form the backbone of most APIs. Let's implement comprehensive CRUD functionality for our User model:

Enhanced app/routes/users.py: `python from flask import Blueprint, jsonify, request from app import db from app.models.user import User from flask_jwt_extended import jwt_required, get_jwt_identity from sqlalchemy.exc import IntegrityError

users_bp = Blueprint('users', __name__)

CREATE - Add a new user

@users_bp.route('/', methods=['POST']) def create_user(): try: data = request.get_json() # Validate required fields required_fields = ['username', 'email', 'password'] for field in required_fields: if field not in data or not data[field]: return jsonify({ 'error': f'{field} is required' }), 400 # Create new user user = User( username=data['username'], email=data['email'], password=data['password'], first_name=data.get('first_name'), last_name=data.get('last_name') ) db.session.add(user) db.session.commit() return jsonify({ 'message': 'User created successfully', 'user': user.to_dict() }), 201 except IntegrityError: db.session.rollback() return jsonify({ 'error': 'Username or email already exists' }), 409 except Exception as e: db.session.rollback() return jsonify({ 'error': 'An error occurred while creating the user' }), 500

READ - Get all users

@users_bp.route('/', methods=['GET']) @jwt_required() def get_all_users(): try: page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) users = User.query.paginate( page=page, per_page=per_page, error_out=False ) return jsonify({ 'users': [user.to_dict() for user in users.items], 'pagination': { 'page': users.page, 'pages': users.pages, 'per_page': users.per_page, 'total': users.total } }), 200 except Exception as e: return jsonify({ 'error': 'An error occurred while fetching users' }), 500

READ - Get a specific user

@users_bp.route('/', methods=['GET']) @jwt_required() def get_user(user_id): try: user = User.query.get_or_404(user_id) return jsonify({ 'user': user.to_dict() }), 200 except Exception as e: return jsonify({ 'error': 'User not found' }), 404

UPDATE - Update a user

@users_bp.route('/', methods=['PUT']) @jwt_required() def update_user(user_id): try: user = User.query.get_or_404(user_id) data = request.get_json() # Check if current user can update this profile current_user_id = get_jwt_identity() if current_user_id != user_id: return jsonify({ 'error': 'Unauthorized to update this user' }), 403 # Update user fields updatable_fields = ['username', 'email', 'first_name', 'last_name'] for field in updatable_fields: if field in data: setattr(user, field, data[field]) # Handle password update separately if 'password' in data and data['password']: user.set_password(data['password']) db.session.commit() return jsonify({ 'message': 'User updated successfully', 'user': user.to_dict() }), 200 except IntegrityError: db.session.rollback() return jsonify({ 'error': 'Username or email already exists' }), 409 except Exception as e: db.session.rollback() return jsonify({ 'error': 'An error occurred while updating the user' }), 500

DELETE - Delete a user

@users_bp.route('/', methods=['DELETE']) @jwt_required() def delete_user(user_id): try: user = User.query.get_or_404(user_id) # Check if current user can delete this profile current_user_id = get_jwt_identity() if current_user_id != user_id: return jsonify({ 'error': 'Unauthorized to delete this user' }), 403 db.session.delete(user) db.session.commit() return jsonify({ 'message': 'User deleted successfully' }), 200 except Exception as e: db.session.rollback() return jsonify({ 'error': 'An error occurred while deleting the user' }), 500 `

Implementing JWT Authentication

JSON Web Tokens (JWT) provide a secure way to authenticate API requests. Let's implement comprehensive authentication:

app/routes/auth.py: `python from flask import Blueprint, jsonify, request from app import db from app.models.user import User from flask_jwt_extended import ( create_access_token, create_refresh_token, jwt_required, get_jwt_identity, get_jwt ) from datetime import datetime, timedelta

auth_bp = Blueprint('auth', __name__)

Token blacklist (in production, use Redis or database)

blacklisted_tokens = set()

@auth_bp.route('/register', methods=['POST']) def register(): try: data = request.get_json() # Validate required fields required_fields = ['username', 'email', 'password'] for field in required_fields: if field not in data or not data[field]: return jsonify({ 'error': f'{field} is required' }), 400 # Check if user already exists if User.query.filter_by(username=data['username']).first(): return jsonify({ 'error': 'Username already exists' }), 409 if User.query.filter_by(email=data['email']).first(): return jsonify({ 'error': 'Email already exists' }), 409 # Create new user user = User( username=data['username'], email=data['email'], password=data['password'], first_name=data.get('first_name'), last_name=data.get('last_name') ) db.session.add(user) db.session.commit() # Create tokens access_token = create_access_token(identity=user.id) refresh_token = create_refresh_token(identity=user.id) return jsonify({ 'message': 'User registered successfully', 'access_token': access_token, 'refresh_token': refresh_token, 'user': user.to_dict() }), 201 except Exception as e: db.session.rollback() return jsonify({ 'error': 'An error occurred during registration' }), 500

@auth_bp.route('/login', methods=['POST']) def login(): try: data = request.get_json() # Validate required fields if not data.get('username') or not data.get('password'): return jsonify({ 'error': 'Username and password are required' }), 400 # Find user by username or email user = User.query.filter( (User.username == data['username']) | (User.email == data['username']) ).first() if not user or not user.check_password(data['password']): return jsonify({ 'error': 'Invalid credentials' }), 401 if not user.is_active: return jsonify({ 'error': 'Account is deactivated' }), 401 # Create tokens access_token = create_access_token(identity=user.id) refresh_token = create_refresh_token(identity=user.id) return jsonify({ 'message': 'Login successful', 'access_token': access_token, 'refresh_token': refresh_token, 'user': user.to_dict() }), 200 except Exception as e: return jsonify({ 'error': 'An error occurred during login' }), 500

@auth_bp.route('/refresh', methods=['POST']) @jwt_required(refresh=True) def refresh(): try: current_user_id = get_jwt_identity() user = User.query.get(current_user_id) if not user or not user.is_active: return jsonify({ 'error': 'User not found or inactive' }), 404 new_access_token = create_access_token(identity=current_user_id) return jsonify({ 'access_token': new_access_token }), 200 except Exception as e: return jsonify({ 'error': 'An error occurred while refreshing token' }), 500

@auth_bp.route('/logout', methods=['POST']) @jwt_required() def logout(): try: jti = get_jwt()['jti'] blacklisted_tokens.add(jti) return jsonify({ 'message': 'Successfully logged out' }), 200 except Exception as e: return jsonify({ 'error': 'An error occurred during logout' }), 500

@auth_bp.route('/me', methods=['GET']) @jwt_required() def get_current_user(): try: current_user_id = get_jwt_identity() user = User.query.get(current_user_id) if not user: return jsonify({ 'error': 'User not found' }), 404 return jsonify({ 'user': user.to_dict() }), 200 except Exception as e: return jsonify({ 'error': 'An error occurred while fetching user data' }), 500 `

Advanced Authentication Features

Let's enhance our authentication system with additional security features:

Enhanced app/__init__.py with JWT callbacks: `python from flask import Flask, jsonify from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import JWTManager from config import Config

Initialize extensions

db = SQLAlchemy() jwt = JWTManager()

def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) # Initialize extensions with app db.init_app(app) jwt.init_app(app) # JWT error handlers @jwt.expired_token_loader def expired_token_callback(jwt_header, jwt_payload): return jsonify({ 'error': 'Token has expired', 'message': 'Please refresh your token' }), 401 @jwt.invalid_token_loader def invalid_token_callback(error): return jsonify({ 'error': 'Invalid token', 'message': 'Please provide a valid token' }), 401 @jwt.unauthorized_loader def missing_token_callback(error): return jsonify({ 'error': 'Authorization token is required', 'message': 'Please provide an access token' }), 401 @jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): from app.routes.auth import blacklisted_tokens jti = jwt_payload['jti'] return jti in blacklisted_tokens @jwt.revoked_token_loader def revoked_token_callback(jwt_header, jwt_payload): return jsonify({ 'error': 'Token has been revoked', 'message': 'Please login again' }), 401 # Register blueprints from app.routes.auth import auth_bp from app.routes.users import users_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(users_bp, url_prefix='/api/users') # Global error handlers @app.errorhandler(404) def not_found(error): return jsonify({ 'error': 'Resource not found' }), 404 @app.errorhandler(500) def internal_error(error): db.session.rollback() return jsonify({ 'error': 'Internal server error' }), 500 return app `

Input Validation and Error Handling

Proper validation and error handling are crucial for API security and user experience:

app/utils/validators.py: `python import re from functools import wraps from flask import request, jsonify

def validate_email(email): """Validate email format""" pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

Build REST API in Python with Flask: Complete Guide

return re.match(pattern, email) is not None

def validate_password(password): """Validate password strength""" if len(password) < 8: return False, "Password must be at least 8 characters long" if not re.search(r'[A-Z]', password): return False, "Password must contain at least one uppercase letter" if not re.search(r'[a-z]', password): return False, "Password must contain at least one lowercase letter" if not re.search(r'\d', password): return False, "Password must contain at least one digit" return True, "Password is valid"

def validate_json(*required_fields): """Decorator to validate JSON input""" def decorator(f): @wraps(f) def decorated_function(args, *kwargs): if not request.is_json: return jsonify({ 'error': 'Content-Type must be application/json' }), 400 data = request.get_json() if not data: return jsonify({ 'error': 'No JSON data provided' }), 400 # Check required fields missing_fields = [] for field in required_fields: if field not in data or not data[field]: missing_fields.append(field) if missing_fields: return jsonify({ 'error': f'Missing required fields: {", ".join(missing_fields)}' }), 400 return f(args, *kwargs) return decorated_function return decorator `

Testing Your API

Testing is essential for maintaining API reliability. Here's how to test your endpoints:

test_api.py: `python import unittest import json from app import create_app, db from app.models.user import User

class APITestCase(unittest.TestCase): def setUp(self): self.app = create_app() self.app.config['TESTING'] = True self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' self.client = self.app.test_client() with self.app.app_context(): db.create_all() def tearDown(self): with self.app.app_context(): db.session.remove() db.drop_all() def test_user_registration(self): """Test user registration""" response = self.client.post('/api/auth/register', data=json.dumps({ 'username': 'testuser', 'email': 'test@example.com', 'password': 'TestPass123' }), content_type='application/json' ) self.assertEqual(response.status_code, 201) data = json.loads(response.data) self.assertIn('access_token', data) self.assertIn('user', data) def test_user_login(self): """Test user login""" # First register a user with self.app.app_context(): user = User('testuser', 'test@example.com', 'TestPass123') db.session.add(user) db.session.commit() response = self.client.post('/api/auth/login', data=json.dumps({ 'username': 'testuser', 'password': 'TestPass123' }), content_type='application/json' ) self.assertEqual(response.status_code, 200) data = json.loads(response.data) self.assertIn('access_token', data)

if __name__ == '__main__': unittest.main() `

API Documentation and Best Practices

Document your API endpoints for better developer experience:

API Documentation Example:

`

User Management API

Authentication Endpoints

POST /api/auth/register

Register a new user account.

Request Body: `json { "username": "string (required)", "email": "string (required)", "password": "string (required)", "first_name": "string (optional)", "last_name": "string (optional)" } `

Response (201): `json { "message": "User registered successfully", "access_token": "jwt_token", "refresh_token": "jwt_refresh_token", "user": { "id": 1, "username": "testuser", "email": "test@example.com" } } `

POST /api/auth/login

Authenticate user and receive access tokens.

Request Body: `json { "username": "string (required)", "password": "string (required)" } ` `

Deployment and Production Considerations

When preparing your Flask API for production:

Environment Configuration (.env): `env SECRET_KEY=your-super-secret-key-here JWT_SECRET_KEY=your-jwt-secret-key DATABASE_URL=postgresql://user:password@localhost/dbname FLASK_ENV=production `

Production-ready run.py: `python from app import create_app, db from dotenv import load_dotenv import os

load_dotenv()

app = create_app()

if __name__ == '__main__': port = int(os.environ.get('PORT', 5000)) app.run(host='0.0.0.0', port=port, debug=False) `

Conclusion

You've successfully built a comprehensive REST API with Flask that includes:

- Complete CRUD operations for user management - JWT-based authentication and authorization - Input validation and error handling - Proper project structure and organization - Database integration with SQLAlchemy - Security best practices

This foundation provides everything you need to build scalable, secure REST APIs with Flask. Remember to always validate input, handle errors gracefully, implement proper authentication, and thoroughly test your endpoints before deployment.

The modular structure we've created makes it easy to extend your API with additional features like role-based permissions, file uploads, email notifications, and more complex business logic. Continue building upon this foundation to create robust, production-ready APIs that can handle real-world applications.

Tags

  • Flask
  • Python
  • REST API
  • SQLAlchemy
  • Web Development

Related Articles

Related Books - Expand Your Knowledge

Explore these Python books to deepen your understanding:

Browse all IT books

Popular Technical Articles & Tutorials

Explore our comprehensive collection of technical articles, programming tutorials, and IT guides written by industry experts:

Browse all 8+ technical articles | Read our IT blog

Build REST API in Python with Flask: Complete Guide