Full-Stack API Project
Learning Objectives
- By the end of this project, you will be able to:
- - Build a complete REST API from scratch
- - Integrate databases with APIs
- - Implement authentication and authorization
- - Create API documentation
- - Handle errors and validation
- - Structure a full-stack API project
- - Use Flask-RESTful or FastAPI
- - Implement JWT authentication
- - Create database models
- - Write comprehensive tests
- - Deploy APIs
- - Apply API best practices
- - Debug API issues
Project 5.1: Full-Stack API Project
Learning Objectives
By the end of this project, you will be able to:
- Build a complete REST API from scratch
- Integrate databases with APIs
- Implement authentication and authorization
- Create API documentation
- Handle errors and validation
- Structure a full-stack API project
- Use Flask-RESTful or FastAPI
- Implement JWT authentication
- Create database models
- Write comprehensive tests
- Deploy APIs
- Apply API best practices
- Debug API issues
Introduction to Full-Stack API Development
Full-Stack API development involves creating a complete backend API that can serve frontend applications, mobile apps, or other services. It includes database integration, authentication, error handling, and documentation.
Key Components:
- REST API: RESTful endpoints
- Database: Data persistence
- Authentication: User authentication and authorization
- Validation: Input validation and error handling
- Documentation: API documentation
- Testing: Unit and integration tests
Project Structure:
project/
├── app/
│ ├── __init__.py
│ ├── models.py # Database models
│ ├── routes.py # API routes
│ ├── auth.py # Authentication
│ ├── config.py # Configuration
│ └── utils.py # Utilities
├── tests/ # Tests
├── requirements.txt # Dependencies
├── README.md # Documentation
└── run.py # Application entry point
Complete REST API
Project Setup
# requirements.txt
flask==2.3.0
flask-restful==0.3.10
flask-jwt-extended==4.5.0
flask-sqlalchemy==3.0.5
flask-migrate==4.0.4
flask-cors==4.0.0
python-dotenv==1.0.0
marshmallow==3.20.0
Application Structure
# app/__init__.py
from flask import Flask
from flask_restful import Api
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
jwt = JWTManager()
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
jwt.init_app(app)
migrate.init_app(app, db)
CORS(app)
# Register blueprints/routes
from app.routes import api_bp
app.register_blueprint(api_bp)
return app
Configuration
# config.py
import os
from datetime import timedelta
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
# JWT Configuration
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-key'
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30)
# CORS
CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*').split(',')
Database Integration
Models
# app/models.py
from app import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(128), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
# Relationships
posts = db.relationship('Post', backref='author', lazy=True, cascade='all, delete-orphan')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat(),
'is_active': self.is_active
}
def __repr__(self):
return f'<User {self.username}>'
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
published = db.Column(db.Boolean, default=False, index=True)
def to_dict(self):
return {
'id': self.id,
'title': self.title,
'content': self.content,
'author_id': self.author_id,
'author': self.author.username if self.author else None,
'created_at': self.created_at.isoformat(),
'updated_at': self.updated_at.isoformat(),
'published': self.published
}
def __repr__(self):
return f'<Post {self.title}>'
Database Initialization
# app/__init__.py (continued)
def create_app(config_class=Config):
# ... previous code ...
# Create tables
with app.app_context():
db.create_all()
return app
Authentication
JWT Authentication
# app/auth.py
from flask_jwt_extended import create_access_token, create_refresh_token
from app.models import User
from app import db
def authenticate(username, password):
"""Authenticate user and return tokens."""
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
access_token = create_access_token(identity=user.id)
refresh_token = create_refresh_token(identity=user.id)
return {
'access_token': access_token,
'refresh_token': refresh_token,
'user': user.to_dict()
}
return None
def register_user(username, email, password):
"""Register a new user."""
if User.query.filter_by(username=username).first():
return {'error': 'Username already exists'}, 400
if User.query.filter_by(email=email).first():
return {'error': 'Email already exists'}, 400
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
return {'message': 'User created successfully', 'user': user.to_dict()}, 201
Protected Routes
# app/routes.py
from flask import Blueprint
from flask_restful import Api, Resource, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.models import User, Post, db
from app.auth import authenticate, register_user
api_bp = Blueprint('api', __name__, url_prefix='/api')
api = Api(api_bp)
# Request parsers
login_parser = reqparse.RequestParser()
login_parser.add_argument('username', type=str, required=True)
login_parser.add_argument('password', type=str, required=True)
register_parser = reqparse.RequestParser()
register_parser.add_argument('username', type=str, required=True)
register_parser.add_argument('email', type=str, required=True)
register_parser.add_argument('password', type=str, required=True)
post_parser = reqparse.RequestParser()
post_parser.add_argument('title', type=str, required=True)
post_parser.add_argument('content', type=str, required=True)
post_parser.add_argument('published', type=bool, default=False)
class Login(Resource):
def post(self):
args = login_parser.parse_args()
result = authenticate(args['username'], args['password'])
if result:
return result, 200
return {'error': 'Invalid credentials'}, 401
class Register(Resource):
def post(self):
args = register_parser.parse_args()
return register_user(args['username'], args['email'], args['password'])
class PostList(Resource):
@jwt_required()
def get(self):
"""Get all posts (authenticated users only)."""
posts = Post.query.filter_by(published=True).order_by(Post.created_at.desc()).all()
return {'posts': [post.to_dict() for post in posts]}, 200
@jwt_required()
def post(self):
"""Create a new post."""
current_user_id = get_jwt_identity()
args = post_parser.parse_args()
post = Post(
title=args['title'],
content=args['content'],
author_id=current_user_id,
published=args.get('published', False)
)
db.session.add(post)
db.session.commit()
return post.to_dict(), 201
class PostDetail(Resource):
@jwt_required()
def get(self, post_id):
"""Get a specific post."""
post = Post.query.get_or_404(post_id)
return post.to_dict(), 200
@jwt_required()
def put(self, post_id):
"""Update a post (owner only)."""
current_user_id = get_jwt_identity()
post = Post.query.get_or_404(post_id)
if post.author_id != current_user_id:
return {'error': 'Permission denied'}, 403
args = post_parser.parse_args()
post.title = args['title']
post.content = args['content']
post.published = args.get('published', post.published)
db.session.commit()
return post.to_dict(), 200
@jwt_required()
def delete(self, post_id):
"""Delete a post (owner only)."""
current_user_id = get_jwt_identity()
post = Post.query.get_or_404(post_id)
if post.author_id != current_user_id:
return {'error': 'Permission denied'}, 403
db.session.delete(post)
db.session.commit()
return {'message': 'Post deleted'}, 204
# Register resources
api.add_resource(Login, '/login')
api.add_resource(Register, '/register')
api.add_resource(PostList, '/posts')
api.add_resource(PostDetail, '/posts/<int:post_id>')
Complete API Project
Full Application
# run.py
from app import create_app, db
from app.models import User, Post
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
if __name__ == '__main__':
app.run(debug=True)
Enhanced Routes with Error Handling
# app/routes.py (enhanced)
from flask import Blueprint
from flask_restful import Api, Resource, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity
from werkzeug.exceptions import NotFound, BadRequest
from app.models import User, Post, db
from app.auth import authenticate, register_user
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
api = Api(api_bp)
# Error handlers
@api_bp.errorhandler(404)
def not_found(error):
return {'error': 'Resource not found'}, 404
@api_bp.errorhandler(400)
def bad_request(error):
return {'error': 'Bad request'}, 400
@api_bp.errorhandler(500)
def internal_error(error):
db.session.rollback()
return {'error': 'Internal server error'}, 500
# Parsers
login_parser = reqparse.RequestParser()
login_parser.add_argument('username', type=str, required=True, help='Username is required')
login_parser.add_argument('password', type=str, required=True, help='Password is required')
register_parser = reqparse.RequestParser()
register_parser.add_argument('username', type=str, required=True, help='Username is required')
register_parser.add_argument('email', type=str, required=True, help='Email is required')
register_parser.add_argument('password', type=str, required=True, help='Password is required')
post_parser = reqparse.RequestParser()
post_parser.add_argument('title', type=str, required=True, help='Title is required')
post_parser.add_argument('content', type=str, required=True, help='Content is required')
post_parser.add_argument('published', type=bool, default=False)
class Login(Resource):
def post(self):
"""User login endpoint."""
args = login_parser.parse_args()
result = authenticate(args['username'], args['password'])
if result:
return result, 200
return {'error': 'Invalid username or password'}, 401
class Register(Resource):
def post(self):
"""User registration endpoint."""
args = register_parser.parse_args()
return register_user(args['username'], args['email'], args['password'])
class PostList(Resource):
@jwt_required()
def get(self):
"""Get all published posts."""
posts = Post.query.filter_by(published=True).order_by(Post.created_at.desc()).all()
return {
'posts': [post.to_dict() for post in posts],
'count': len(posts)
}, 200
@jwt_required()
def post(self):
"""Create a new post."""
current_user_id = get_jwt_identity()
args = post_parser.parse_args()
try:
post = Post(
title=args['title'],
content=args['content'],
author_id=current_user_id,
published=args.get('published', False)
)
db.session.add(post)
db.session.commit()
return post.to_dict(), 201
except Exception as e:
db.session.rollback()
return {'error': str(e)}, 500
class PostDetail(Resource):
@jwt_required()
def get(self, post_id):
"""Get a specific post."""
post = Post.query.get_or_404(post_id)
return post.to_dict(), 200
@jwt_required()
def put(self, post_id):
"""Update a post."""
current_user_id = get_jwt_identity()
post = Post.query.get_or_404(post_id)
if post.author_id != current_user_id:
return {'error': 'Permission denied'}, 403
args = post_parser.parse_args()
post.title = args['title']
post.content = args['content']
post.published = args.get('published', post.published)
try:
db.session.commit()
return post.to_dict(), 200
except Exception as e:
db.session.rollback()
return {'error': str(e)}, 500
@jwt_required()
def delete(self, post_id):
"""Delete a post."""
current_user_id = get_jwt_identity()
post = Post.query.get_or_404(post_id)
if post.author_id != current_user_id:
return {'error': 'Permission denied'}, 403
try:
db.session.delete(post)
db.session.commit()
return {'message': 'Post deleted successfully'}, 200
except Exception as e:
db.session.rollback()
return {'error': str(e)}, 500
class UserPosts(Resource):
@jwt_required()
def get(self, user_id):
"""Get all posts by a specific user."""
user = User.query.get_or_404(user_id)
posts = Post.query.filter_by(author_id=user_id).order_by(Post.created_at.desc()).all()
return {
'user': user.to_dict(),
'posts': [post.to_dict() for post in posts],
'count': len(posts)
}, 200
class UserProfile(Resource):
@jwt_required()
def get(self):
"""Get current user profile."""
current_user_id = get_jwt_identity()
user = User.query.get_or_404(current_user_id)
return user.to_dict(), 200
# Register resources
api.add_resource(Login, '/auth/login')
api.add_resource(Register, '/auth/register')
api.add_resource(PostList, '/posts')
api.add_resource(PostDetail, '/posts/<int:post_id>')
api.add_resource(UserPosts, '/users/<int:user_id>/posts')
api.add_resource(UserProfile, '/users/me')
Documentation
API Documentation
# app/docs.py
from flask import Blueprint
from flask_restful import Api, Resource
docs_bp = Blueprint('docs', __name__)
api = Api(docs_bp)
class APIDocs(Resource):
def get(self):
"""API Documentation."""
return {
'title': 'Blog API',
'version': '1.0',
'endpoints': {
'authentication': {
'POST /api/v1/auth/login': {
'description': 'User login',
'parameters': {
'username': 'string (required)',
'password': 'string (required)'
},
'response': {
'access_token': 'JWT token',
'refresh_token': 'Refresh token',
'user': 'User object'
}
},
'POST /api/v1/auth/register': {
'description': 'User registration',
'parameters': {
'username': 'string (required)',
'email': 'string (required)',
'password': 'string (required)'
}
}
},
'posts': {
'GET /api/v1/posts': {
'description': 'Get all published posts',
'authentication': 'Required',
'response': 'List of posts'
},
'POST /api/v1/posts': {
'description': 'Create a new post',
'authentication': 'Required',
'parameters': {
'title': 'string (required)',
'content': 'string (required)',
'published': 'boolean (optional)'
}
},
'GET /api/v1/posts/<id>': {
'description': 'Get a specific post',
'authentication': 'Required'
},
'PUT /api/v1/posts/<id>': {
'description': 'Update a post (owner only)',
'authentication': 'Required'
},
'DELETE /api/v1/posts/<id>': {
'description': 'Delete a post (owner only)',
'authentication': 'Required'
}
},
'users': {
'GET /api/v1/users/me': {
'description': 'Get current user profile',
'authentication': 'Required'
},
'GET /api/v1/users/<id>/posts': {
'description': 'Get all posts by a user',
'authentication': 'Required'
}
}
},
'authentication': {
'type': 'JWT Bearer Token',
'header': 'Authorization: Bearer <token>'
}
}
api.add_resource(APIDocs, '/docs')
Using Flask-RESTX for Swagger
# Install: pip install flask-restx
from flask_restx import Api, Resource, fields
from flask import Blueprint
docs_bp = Blueprint('api', __name__, url_prefix='/api')
api = Api(docs_bp, doc='/swagger/', version='1.0', title='Blog API')
# Define models
user_model = api.model('User', {
'id': fields.Integer(readonly=True),
'username': fields.String(required=True),
'email': fields.String(required=True),
'created_at': fields.DateTime(readonly=True)
})
post_model = api.model('Post', {
'id': fields.Integer(readonly=True),
'title': fields.String(required=True),
'content': fields.String(required=True),
'author_id': fields.Integer(readonly=True),
'published': fields.Boolean(default=False),
'created_at': fields.DateTime(readonly=True)
})
# Document endpoints
@api.route('/posts')
class PostList(Resource):
@api.doc('list_posts')
@api.marshal_list_with(post_model)
@jwt_required()
def get(self):
"""List all posts"""
posts = Post.query.filter_by(published=True).all()
return posts
@api.doc('create_post')
@api.expect(post_model)
@api.marshal_with(post_model, code=201)
@jwt_required()
def post(self):
"""Create a new post"""
# Implementation
pass
Testing
Unit Tests
# tests/test_api.py
import unittest
from app import create_app, db
from app.models import User, Post
from flask_jwt_extended import create_access_token
class APITestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
self.client = self.app.test_client()
db.create_all()
# Create test user
self.user = User(username='testuser', email='test@example.com')
self.user.set_password('testpass')
db.session.add(self.user)
db.session.commit()
# Create access token
self.token = create_access_token(identity=self.user.id)
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_register(self):
"""Test user registration."""
response = self.client.post('/api/v1/auth/register', json={
'username': 'newuser',
'email': 'newuser@example.com',
'password': 'password123'
})
self.assertEqual(response.status_code, 201)
self.assertIn('user', response.json)
def test_login(self):
"""Test user login."""
response = self.client.post('/api/v1/auth/login', json={
'username': 'testuser',
'password': 'testpass'
})
self.assertEqual(response.status_code, 200)
self.assertIn('access_token', response.json)
def test_create_post(self):
"""Test creating a post."""
response = self.client.post('/api/v1/posts',
headers={'Authorization': f'Bearer {self.token}'},
json={
'title': 'Test Post',
'content': 'Test content',
'published': True
}
)
self.assertEqual(response.status_code, 201)
self.assertIn('title', response.json)
def test_get_posts(self):
"""Test getting posts."""
# Create a post
post = Post(title='Test', content='Content', author_id=self.user.id, published=True)
db.session.add(post)
db.session.commit()
response = self.client.get('/api/v1/posts',
headers={'Authorization': f'Bearer {self.token}'}
)
self.assertEqual(response.status_code, 200)
self.assertIn('posts', response.json)
if __name__ == '__main__':
unittest.main()
Practice Exercise
Exercise: Full API Project
Objective: Create a complete full-stack API project.
Requirements:
-
Create a REST API with:
- User authentication (register, login)
- CRUD operations for a resource (e.g., tasks, notes, products)
- Database integration
- Error handling
- API documentation
-
Features:
- JWT authentication
- User registration and login
- Protected routes
- Resource ownership
- Input validation
- Error handling
- API documentation
Example Solution:
#!/usr/bin/env python3
"""
Complete Full-Stack API Project
Task Management API with authentication
"""
# See complete implementation above in "Complete API Project" section
# Additional features to implement:
# - Pagination
# - Filtering and searching
# - File uploads
# - Email notifications
# - Rate limiting
# - Caching
Expected Output: A complete, production-ready REST API with authentication, database integration, and documentation.
Challenge (Optional):
- Add pagination
- Add filtering and search
- Add file uploads
- Add email notifications
- Add rate limiting
- Add caching with Redis
- Create frontend client
- Deploy to cloud
Key Takeaways
- Full-Stack API - Complete backend API
- REST API - RESTful endpoints
- Database Integration - SQLAlchemy ORM
- Authentication - JWT tokens
- Authorization - Role-based access
- Error Handling - Proper error responses
- Validation - Input validation
- Documentation - API documentation
- Testing - Unit and integration tests
- Project Structure - Organized codebase
- Best Practices - Security, performance, maintainability
- Deployment - Production deployment
- Versioning - API versioning
- CORS - Cross-origin resource sharing
- Security - Authentication, authorization, validation
Quiz: API Development
Test your understanding with these questions:
-
What is a full-stack API?
- A) Frontend only
- B) Complete backend API
- C) Database only
- D) Frontend and backend
-
What is JWT used for?
- A) Database queries
- B) Authentication
- C) File storage
- D) API documentation
-
What integrates database with API?
- A) SQLAlchemy
- B) Flask
- C) requests
- D) BeautifulSoup
-
What protects routes?
- A) @protected
- B) @jwt_required
- C) @auth
- D) @secure
-
What validates input?
- A) reqparse
- B) validator
- C) parser
- D) checker
-
What documents APIs?
- A) Swagger
- B) OpenAPI
- C) Flask-RESTX
- D) All of the above
-
What is API versioning?
- A) /api/v1/
- B) /api/v2/
- C) URL prefix
- D) All of the above
-
What handles errors?
- A) Error handlers
- B) Try-except
- C) Validation
- D) All of the above
-
What is CORS?
- A) Cross-origin resource sharing
- B) Security feature
- C) Allows frontend access
- D) All of the above
-
What tests APIs?
- A) unittest
- B) pytest
- C) Integration tests
- D) All of the above
Answers:
- B) Complete backend API (full-stack API)
- B) Authentication (JWT purpose)
- A) SQLAlchemy (database integration)
- B) @jwt_required (protect routes)
- A) reqparse (input validation)
- D) All of the above (API documentation)
- D) All of the above (API versioning)
- D) All of the above (error handling)
- D) All of the above (CORS)
- D) All of the above (testing)
Next Steps
Excellent work! You've mastered full-stack API development. You now understand:
- Complete REST API
- Database integration
- Authentication
- Documentation
- How to build production APIs
Congratulations! You've completed the Python course!
Additional Resources
- Flask Documentation: flask.palletsprojects.com/
- Flask-RESTful: flask-restful.readthedocs.io/
- SQLAlchemy: docs.sqlalchemy.org/
- JWT: jwt.io/
- API Design: Best practices for REST APIs
- Deployment: Deploying Flask applications
Project completed! You've completed the full Python course. Congratulations!
Course Navigation
- Full-Stack API Project