A modern, fully-featured RESTful API for book management built with FastAPI, SQLAlchemy, PostgreSQL, and * Redis*. Features JWT authentication with token revocation, user management, and book CRUD operations.
- π JWT Authentication with access & refresh tokens
- πͺ Token Revocation using Redis blacklist
- π€ User Management (register, login, profile, password change)
- π Book CRUD Operations with user ownership
- β Review System with ratings and user reviews
- π§ Email Verification with password reset functionality
- π Async/Await throughout the application
- π Database Migrations with Alembic
- π Auto-generated API Documentation (Swagger UI & ReDoc)
- π‘οΈ Security Best Practices (password hashing, secure tokens)
- π’ API Versioning with multiple API versions (V1 & V2)
- π Pagination Support in V2 API with search and filtering
- π OAuth2 Social Login - Google and GitHub authentication
- π File Upload/Download - Image and document handling with streaming
- π WebSockets - Real-time communication for notifications and chat
- β‘ Rate Limiting - Request throttling with Redis backend
- π Background Tasks - Async task processing with Celery
- π GraphQL - Alternative GraphQL API (Strawberry integration)
FastAPI-Books_RestAPI/
βββ alembic/ # Database migrations
β βββ versions/ # Migration files
β βββ env.py # Alembic configuration
βββ app/
β βββ api/
β β βββ v1/ # API Version 1 (original)
β β β βββ routes/
β β β βββ auth.py # Authentication endpoints
β β β βββ book.py # Book CRUD endpoints
β β β βββ user.py # User management endpoints
β β β βββ admin.py # Admin endpoints
β β β βββ review.py # Review endpoints
β β βββ v2/ # API Version 2 (with pagination)
β β βββ routes/
β β βββ auth.py # Authentication endpoints
β β βββ book.py # Book CRUD with pagination
β β βββ user.py # User management with pagination
β β βββ admin.py # Admin endpoints
β β βββ review.py # Review endpoints with pagination
β βββ core/
β β βββ config.py # Application settings
β β βββ database.py # Database connection
β β βββ redis.py # Redis connection & token blacklist
β β βββ security.py # JWT & password utilities
β βββ models/
β β βββ book.py # Book SQLAlchemy model
β β βββ user.py # User SQLAlchemy model
β β βββ review.py # Review SQLAlchemy model
β βββ schemas/
β β βββ auth.py # Authentication Pydantic schemas
β β βββ book.py # Book Pydantic schemas
β β βββ user.py # User Pydantic schemas
β β βββ review.py # Review Pydantic schemas
β β βββ pagination.py # Pagination Pydantic schemas
β βββ services/
β β βββ auth_services.py # Authentication business logic
β β βββ book_service.py # Book business logic
β β βββ user_services.py # User business logic
β β βββ review_service.py # Review business logic
β βββ main.py # FastAPI application entry point
βββ .env # Environment variables (not in repo)
βββ .env.example # Example environment variables
βββ alembic.ini # Alembic configuration
βββ requirements.txt # Python dependencies
βββ README.md # This file
- Python 3.11+
- PostgreSQL 14+
- Redis 7+
-
Clone the repository
git clone https://github.com/yourusername/FastAPI-Books_RestAPI.git cd FastAPI-Books_RestAPI -
Create a virtual environment
python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate
-
Install dependencies
pip install -r requirements.txt
-
Set up environment variables
cp .env.example .env # Edit .env with your configuration -
Configure your
.envfileDATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/booksdb SECRET_KEY=your-super-secret-key-here ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=30 REDIS_HOST=localhost REDIS_PORT=6379
-
Run database migrations
alembic upgrade head
-
Start the development server
fastapi dev # Or: uvicorn app.main:app --reload -
Access the API documentation
- Swagger UI: http://127.0.0.1:8000/docs
- ReDoc: http://127.0.0.1:8000/redoc
This API supports multiple versions using URL path versioning:
| Version | Prefix | Status | Description |
|---|---|---|---|
| V1 | /api/v1 |
Stable | Original API with basic CRUD functionality |
| V2 | /api/v2 |
Stable | Enhanced API with pagination, search & sorting |
V2 endpoints that return lists include pagination support:
{
"items": [
...
],
"total": 100,
"page": 1,
"page_size": 10,
"total_pages": 10,
"has_next": true,
"has_previous": false
}Query Parameters:
page(default: 1) - Page number (1-indexed)page_size(default: 10, max: 100) - Items per pagesearch- Search query (searches in title, author, username, email, etc.)sort_by- Field to sort bysort_order- Sort direction (ascordesc)
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /login |
Login with email & password | β |
| POST | /register |
Create a new account | β |
| POST | /refresh |
Refresh access token | β |
| POST | /logout |
Logout (revoke current token) | β |
| POST | /logout-all |
Logout from all devices | β |
| Method | Endpoint | Description | Auth Required | Role Required |
|---|---|---|---|---|
| GET | /me |
Get current user profile | β | Any |
| GET | / |
List all users | β | - |
| GET | /{user_uuid} |
Get user by UUID | β | - |
| GET | /email/{email} |
Get user by email | β | - |
| PATCH | /{user_uuid} |
Update user profile | β | Any (own) |
| DELETE | /{user_uuid} |
Delete user account | β | Any (own) |
| POST | /change-password |
Change password | β | Any |
| Method | Endpoint | Description | Auth Required | Role Required |
|---|---|---|---|---|
| POST | /admin/create |
Create user with any role | β | Admin |
| PATCH | /admin/{user_uuid} |
Update any user | β | Admin |
| DELETE | /admin/{user_uuid} |
Delete any user | β | Admin |
| PATCH | /admin/{user_uuid}/role |
Change user role | β | Admin |
| PATCH | /admin/{user_uuid}/activate |
Activate/deactivate user | β | Admin |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | / |
List all books | β |
| GET | /my-books |
List current user's books | β |
| GET | /{book_uuid} |
Get book by UUID | β |
| POST | / |
Create a new book | β |
| PATCH | /{book_uuid} |
Update a book | β |
| DELETE | /{book_uuid} |
Delete a book | β |
The API implements three user roles with different permission levels:
| Role | Description | Permissions |
|---|---|---|
USER |
Default role for registered users | CRUD own profile, CRUD own books |
MODERATOR |
Elevated privileges | All USER permissions + moderate content |
ADMIN |
Full system access | All permissions + manage users & roles |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ROLE HIERARCHY β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ADMIN (Full Access) β
β β β
β βββ Create users with any role β
β βββ Update any user (including role) β
β βββ Delete any user β
β βββ Activate/deactivate users β
β βββ All MODERATOR permissions β
β β
β MODERATOR β
β β β
β βββ Moderate content (future feature) β
β βββ All USER permissions β
β β
β USER (Default) β
β β β
β βββ CRUD own profile β
β βββ CRUD own books β
β βββ Change own password β
β βββ View public resources β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The API supports social login with Google and GitHub.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/oauth/providers |
List configured OAuth providers |
| GET | /api/v1/oauth/{provider}/authorize |
Start OAuth flow |
| GET | /api/v1/oauth/{provider}/callback |
Handle OAuth callback |
Add these to your .env file:
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub OAuth
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
OAUTH_REDIRECT_BASE_URL=http://localhost:8000- User visits
/api/v1/oauth/google/authorizeor/api/v1/oauth/github/authorize - User is redirected to the OAuth provider for authentication
- After authentication, user is redirected back to the callback URL
- API creates/logs in user and returns JWT tokens
The API supports file uploads with streaming downloads.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/files/upload |
Upload a single file |
| POST | /api/v1/files/upload/multiple |
Upload multiple files |
| GET | /api/v1/files/my-files |
List user's files |
| GET | /api/v1/files/{user_id}/{filename} |
Download a file |
| GET | /api/v1/files/{user_id}/{filename}/stream |
Stream a file |
| DELETE | /api/v1/files/{filename} |
Delete a file |
curl -X POST "http://localhost:8000/api/v1/files/upload" \
-H "Authorization: Bearer <token>" \
-F "file=@/path/to/image.jpg" \
-F "category=image"- Images:
.jpg,.jpeg,.png,.gif,.webp - Documents:
.pdf,.doc,.docx,.txt,.md - Max Size: 10MB per file
Real-time communication via WebSocket connections.
const ws = new WebSocket('ws://localhost:8000/ws/connect?token=<jwt_token>');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};| Action | Description |
|---|---|
ping |
Health check / keep alive |
join_room |
Join a chat room |
leave_room |
Leave a chat room |
send_message |
Send message to room/all |
typing |
Send typing indicator |
get_online_users |
Get list of online users |
get_room_users |
Get users in a specific room |
// Join a room
{"action": "join_room", "room_id": "book-discussion-123"}
// Send a message to a room
{"action": "send_message", "room_id": "book-discussion-123", "data": {"content": "Hello!"}}
// Send a broadcast message
{"action": "send_message", "data": {"content": "Hello everyone!"}}API endpoints are protected with rate limiting using Redis.
| Endpoint Type | Limit |
|---|---|
| Auth (login) | 5/minute |
| File upload | 10/minute |
| General API | 60/minute |
| Search | 30/minute |
When rate limited, responses include:
X-RateLimit-Limit: Maximum requests allowedX-RateLimit-Remaining: Remaining requestsRetry-After: Seconds until limit resets
{
"detail": "Too many requests",
"error": "rate_limit_exceeded",
"retry_after": "60",
"message": "Rate limit exceeded. Please try again later."
}The API uses Celery for background task processing.
# Start worker
celery -A app.core.celery_app worker --loglevel=info
# Start beat scheduler (for periodic tasks)
celery -A app.core.celery_app beat --loglevel=info| Task | Description |
|---|---|
send_email_task |
Send emails asynchronously |
process_file_task |
Process uploaded files |
cleanup_expired_tokens |
Clean up expired tokens (hourly) |
send_daily_digest |
Send daily digest (daily) |
send_notification_task |
Send user notifications |
generate_report_task |
Generate reports asynchronously |
CELERY_BROKER_URL=redis://localhost:6379/2
CELERY_RESULT_BACKEND=redis://localhost:6379/3An alternative GraphQL endpoint is available (when Strawberry is compatible).
- GraphQL:
http://localhost:8000/graphql - GraphQL Playground:
http://localhost:8000/graphql
# Get paginated books
query {
books(page: 1, pageSize: 10, search: "Python") {
items {
uuid
title
author
}
total
totalPages
hasNext
}
}
# Get book with reviews
query {
bookWithReviews(uuid: "book-uuid") {
book {
title
author
}
reviews {
content
rating
reviewer {
username
}
}
averageRating
reviewCount
}
}# Create a book (requires auth)
mutation {
createBook(input: {
title: "FastAPI Guide"
author: "John Doe"
pages: 300
}) {
success
message
book {
uuid
title
}
}
}from app.core.security import (
get_admin_user,
get_moderator_user,
RoleChecker,
require_admin,
require_moderator,
check_resource_ownership_or_admin
)
from app.models.user import UserRole
# Method 1: Use pre-configured dependencies
@router.get("/admin-only")
async def admin_endpoint(user: User = Depends(get_admin_user)):
...
# Method 2: Use RoleChecker with custom roles
@router.get("/mod-or-admin")
async def mod_endpoint(user: User = Depends(RoleChecker([UserRole.MODERATOR, UserRole.ADMIN]))):
...
# Method 3: Use pre-configured role checkers
@router.get("/admin-only-v2")
async def admin_v2(user: User = Depends(require_admin)):
...
# Method 4: Check resource ownership
@router.patch("/books/{book_id}")
async def update_book(book_id: int, current_user: User = Depends(get_current_active_user)):
book = await get_book(book_id)
check_resource_ownership_or_admin(book.user_id, current_user)
...The security module provides comprehensive authentication and authorization utilities.
from app.core.security import get_password_hash, verify_password
# Hash a password
hashed = get_password_hash("my_password")
# Verify a password
is_valid = verify_password("my_password", hashed)| Function | Description | Expiration |
|---|---|---|
create_access_token(data) |
Creates JWT access token with JTI | 30 minutes (configurable) |
create_refresh_token(data) |
Creates JWT refresh token with JTI | 7 days |
create_verification_token(email) |
Creates email verification token | 24 hours (configurable) |
create_password_reset_token(email) |
Creates password reset token | 1 hour |
| Function | Description | Returns |
|---|---|---|
verify_verification_token(token) |
Validates email verification token | Email or None |
verify_password_reset_token(token) |
Validates password reset token | Email or None |
decode_token(token) |
Decodes and validates any JWT | Payload dict |
| Dependency | Description | Use Case |
|---|---|---|
get_current_user |
Gets user from JWT token | Basic authentication |
get_current_active_user |
Gets active user only | Protected endpoints |
get_current_user_optional |
Gets user without raising errors | GraphQL/Optional auth |
get_admin_user |
Requires admin role | Admin-only endpoints |
get_moderator_user |
Requires moderator or admin | Moderator endpoints |
from app.core.security import require_admin, require_moderator, require_user
# Use as dependencies
@router.get("/admin")
async def admin_only(user: User = Depends(require_admin)):
pass
@router.get("/moderator")
async def mod_only(user: User = Depends(require_moderator)):
pass
@router.get("/user")
async def user_only(user: User = Depends(require_user)):
passfrom app.core.security import RoleChecker
from app.models.user import UserRole
# Create custom role checker
custom_checker = RoleChecker([UserRole.ADMIN, UserRole.MODERATOR])
@router.get("/custom")
async def custom_endpoint(user: User = Depends(custom_checker)):
passfrom app.core.security import check_resource_ownership_or_admin
@router.delete("/books/{book_id}")
async def delete_book(
book_id: int,
current_user: User = Depends(get_current_active_user)
):
book = await get_book(book_id)
# Raises 403 if user doesn't own the book and isn't admin
check_resource_ownership_or_admin(book.user_id, current_user)
await delete_book(book_id)Access Token Claims:
{
"sub": "user-uuid",
"exp": 1709912400,
"iat": 1709910600,
"jti": "unique-token-id",
"type": "access"
}Refresh Token Claims:
{
"sub": "user-uuid",
"exp": 1710515400,
"iat": 1709910600,
"jti": "unique-token-id",
"type": "refresh"
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTHENTICATION FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Register: POST /auth/register β
β βββ Returns: User profile β
β β
β 2. Login: POST /auth/login β
β βββ Returns: { access_token, refresh_token } β
β β
β 3. Access Protected Routes β
β βββ Header: Authorization: Bearer <access_token> β
β β
β 4. Refresh Token: POST /auth/refresh β
β βββ Body: { refresh_token } β
β βββ Returns: New { access_token, refresh_token } β
β β
β 5. Logout: POST /auth/logout β
β βββ Token is blacklisted in Redis β
β β
β 6. Logout All Devices: POST /auth/logout-all β
β βββ All user tokens invalidated via timestamp β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
This API implements JWT token revocation using Redis as a blacklist store:
- Single Token Revocation: When logging out, the token's unique
jti(JWT ID) is stored in Redis - All Devices Logout: Stores a timestamp; all tokens issued before this time are considered invalid
- Automatic Cleanup: Blacklist entries expire when the token would naturally expire (TTL)
# Redis key patterns
blacklist: token:{jti} # Single token revocation
blacklist: user:{uuid} # All devices logout (timestamp-based)- id: Integer(Primary
Key)
- uuid: UUID(Unique, Indexed)
- username: String(Unique)
- email: String(Unique)
- first_name: String
- last_name: String
- role: Enum(ADMIN, MODERATOR, USER) - Default: USER
- password: String(Hashed)
- is_active: Boolean
- created_at: DateTime
- updated_at: DateTime
- books: Relationship β Book[]- id: Integer(Primary
Key)
- uuid: UUID(Unique, Indexed)
- title: String
- author: String
- publisher: String
- publish_date: Date
- pages: Integer
- language: String
- user_id: Integer(Foreign
Key β User)
- created_at: DateTime
- updated_at: DateTime# Install test dependencies
pip install pytest pytest-asyncio httpx
# Run tests
pytest
# Run with coverage
pytest --cov=app tests/# Build and run with Docker Compose
docker-compose up --build| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Required |
SECRET_KEY |
JWT signing key | Required |
ALGORITHM |
JWT algorithm | HS256 |
ACCESS_TOKEN_EXPIRE_MINUTES |
Access token lifetime | 30 |
REDIS_HOST |
Redis server host | localhost |
REDIS_PORT |
Redis server port | 6379 |
MAIL_USERNAME |
Email username | - |
MAIL_PASSWORD |
Email password | - |
MAIL_FROM |
Sender email address | - |
MAIL_PORT |
SMTP port | 587 |
MAIL_SERVER |
SMTP server | - |
FRONTEND_URL |
Frontend URL for links | http://localhost:3000 |
GOOGLE_CLIENT_ID |
Google OAuth client ID | - |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | - |
GITHUB_CLIENT_ID |
GitHub OAuth client ID | - |
GITHUB_CLIENT_SECRET |
GitHub OAuth client secret | - |
OAUTH_REDIRECT_BASE_URL |
OAuth redirect base URL | http://localhost:8000 |
MAX_UPLOAD_SIZE_MB |
Max file upload size | 10 |
CELERY_BROKER_URL |
Celery broker URL | - |
CELERY_RESULT_BACKEND |
Celery result backend | - |
- Framework: FastAPI - Modern, fast web framework
- ORM: SQLAlchemy - SQL toolkit and ORM
- Database: PostgreSQL - Relational database
- Cache/Blacklist: Redis - In-memory data store
- Migrations: Alembic - Database migrations
- Validation: Pydantic - Data validation
- Auth: PyJWT - JWT tokens
- Password Hashing: Passlib with bcrypt
This project is licensed under the MIT License - see the LICENSE file for details.
Vinald
- Email: okiror1vinald@gmail.com
- Website: http://vinald.me
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
β Star this repository if you find it helpful!