A comprehensive FastAPI project demonstrating RESTful API development with authentication, database relationships, and best practices.
π Live Demo Available - Try it out!
- Overview
- Live Demo
- Features
- Project Structure
- Installation
- Development Journey
- API Endpoints
- Database Models
- Authentication
- Deployment to Render
- Credits
- License
This project is a complete blog API built with FastAPI, showcasing modern Python web development practices. It includes user authentication, CRUD operations, database relationships, password hashing, and JWT token-based security.
Original Course: This project is based on the excellent FastAPI Course by Bitfumes, extended with improvements, bug fixes, and enhanced documentation.
π A live version of this API is deployed and ready to use!
π https://fastapi-agrf.onrender.com/docs
You can explore the API interactively using the Swagger UI documentation:
- Create users and blogs
- Test authentication flow
- Try all CRUD operations
- No local setup required!
Note: The free tier on Render may take 30-60 seconds to wake up if it hasn't been accessed recently.
- β RESTful API with FastAPI
- β SQLAlchemy ORM with SQLite
- β User Authentication & Authorization
- β JWT Token-based Security
- β Password Hashing (Argon2)
- β Database Relationships (One-to-Many)
- β Request/Response Models with Pydantic
- β Repository Pattern for Clean Architecture
- β Router-based Modular Design
- β Automatic API Documentation (Swagger UI)
- β Input Validation & Error Handling
fastapi/
βββ blog/
β βββ __init__.py
β βββ main.py # Main application entry point
β βββ database.py # Database connection & session
β βββ models.py # SQLAlchemy ORM models
β βββ schemas.py # Pydantic schemas
β βββ hashing.py # Password hashing utilities
β βββ JWTtoken.py # JWT token generation & verification
β βββ oauth2.py # OAuth2 scheme & authentication
β βββ routers/
β β βββ __init__.py
β β βββ blog.py # Blog endpoints (protected)
β β βββ user.py # User endpoints
β β βββ authentication.py # Login endpoint
β βββ repository/
β βββ blog.py # Blog business logic
β βββ user.py # User business logic
βββ blog.db # SQLite database
βββ requirements.txt # Python dependencies
βββ readme.md # This file
- Python 3.12+
- pip (Python package manager)
-
Clone the repository
git clone https://github.com/DevSsChar/fastapi.git cd fastapi -
Create virtual environment
python -m venv fastapi-env
-
Activate virtual environment
- Windows:
fastapi-env\Scripts\activate
- Linux/Mac:
source fastapi-env/bin/activate
- Windows:
-
Install dependencies
pip install -r requirements.txt
-
Run the application
# Make sure you're in the parent directory (not inside blog/) uvicorn blog.main:app --reload --host 127.0.0.1 --port 8001 -
Access the API
- API: http://127.0.0.1:8001
- Swagger Docs: http://127.0.0.1:8001/docs
- ReDoc: http://127.0.0.1:8001/redoc
This section documents the step-by-step development process with commit checkpoints.
Commit: Initial Information
What was done:
- Project initialization
- Basic FastAPI application setup
- First endpoint created
Files Created:
main.py- Initial FastAPI app
Code:
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
def index():
return {"message": "Hello World"}Theory: FastAPI is a modern, fast web framework for building APIs with Python 3.7+ based on standard Python type hints. It's built on Starlette for web parts and Pydantic for data validation.
Commit: For dynamic endpointwise
What was done:
- Path parameters implementation
- Dynamic route handling
Code Example:
@app.get('/blog/{id}')
def show(id: int):
return {"data": id}Theory:
Path parameters allow you to capture values from the URL path. FastAPI automatically validates and converts the types (e.g., id: int).
Commit: Query Parameters
What was done:
- Query parameter handling
- Optional parameters with defaults
Code Example:
@app.get('/blog')
def index(limit: int = 10, published: bool = True):
if published:
return {"data": f"{limit} published blogs"}
return {"data": f"{limit} blogs"}Theory:
Query parameters are optional key-value pairs that appear after ? in URLs. They're commonly used for filtering, pagination, and search.
Commit: Rqeust body
What was done:
- Pydantic models for request validation
- POST endpoint creation
Files:
schemas.py
Code:
from pydantic import BaseModel
class Blog(BaseModel):
title: str
body: str
@app.post('/blog')
def create(request: Blog):
return {"data": f"Blog created with title: {request.title}"}Theory: Pydantic models provide automatic request validation, serialization, and documentation. FastAPI uses these to validate incoming JSON data.
Commit: Database connection done
What was done:
- SQLAlchemy setup
- Database engine configuration
- Session management
Files:
database.pymodels.py
Code (database.py):
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./blog.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()Code (models.py):
from sqlalchemy import Column, Integer, String
from .database import Base
class Blog(Base):
__tablename__ = "blogs"
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
body = Column(String)Theory:
- SQLAlchemy: Python SQL toolkit and ORM
- Engine: Manages database connections
- Session: Handles database transactions
- Base: Declarative base class for models
Commits: Read and Create operations done, CRUD Operations done
What was done:
- Create (POST) - Add new blogs
- Read (GET) - Retrieve blogs
- Update (PUT) - Modify existing blogs
- Delete (DELETE) - Remove blogs
Code Examples:
# Create
@app.post('/blog')
def create(request: schemas.Blog, db: Session = Depends(get_db)):
new_blog = models.Blog(title=request.title, body=request.body)
db.add(new_blog)
db.commit()
db.refresh(new_blog)
return new_blog
# Read All
@app.get('/blog')
def all(db: Session = Depends(get_db)):
blogs = db.query(models.Blog).all()
return blogs
# Read One
@app.get('/blog/{id}')
def show(id, db: Session = Depends(get_db)):
blog = db.query(models.Blog).filter(models.Blog.id == id).first()
if not blog:
raise HTTPException(status_code=404, detail="Blog not found")
return blog
# Update
@app.put('/blog/{id}')
def update(id, request: schemas.Blog, db: Session = Depends(get_db)):
blog = db.query(models.Blog).filter(models.Blog.id == id)
if not blog.first():
raise HTTPException(status_code=404, detail="Blog not found")
blog.update({'title': request.title, 'body': request.body})
db.commit()
return {"detail": "Blog updated"}
# Delete
@app.delete('/blog/{id}')
def destroy(id, db: Session = Depends(get_db)):
blog = db.query(models.Blog).filter(models.Blog.id == id)
if not blog.first():
raise HTTPException(status_code=404, detail="Blog not found")
blog.delete(synchronize_session=False)
db.commit()
return {"detail": "Blog deleted"}Theory:
- CRUD: Create, Read, Update, Delete - fundamental database operations
- Depends: FastAPI dependency injection for database sessions
- HTTPException: Proper error handling with status codes
Commit: Response models
What was done:
- Pydantic response models for data serialization
- API response standardization
Code (schemas.py):
class ShowBlog(BaseModel):
title: str
body: str
class Config:
from_attributes = True # Allows ORM model conversion
@app.get('/blog/{id}', response_model=ShowBlog)
def show(id, db: Session = Depends(get_db)):
blog = db.query(models.Blog).filter(models.Blog.id == id).first()
return blogTheory:
Response models ensure that only specified fields are returned to clients. The from_attributes = True (formerly orm_mode = True) allows Pydantic to work with SQLAlchemy ORM models.
Commit: User Creation
What was done:
- User model creation
- User registration endpoint
Code (models.py):
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
email = Column(String)
password = Column(String)Code (main.py):
@app.post('/user')
def create_user(request: schemas.User, db: Session = Depends(get_db)):
new_user = models.User(
name=request.name,
email=request.email,
password=request.password
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_userCommit: Password hashing
What was done:
- Password hashing with Argon2
- Secure password storage
Files:
hashing.py
Code:
from passlib.context import CryptContext
pwd_cxt = CryptContext(schemes=["argon2"], deprecated="auto")
class Hash():
def bcrypt(password: str):
return pwd_cxt.hash(password)
def verify(hashed_password: str, plain_password: str):
return pwd_cxt.verify(plain_password, hashed_password)Usage:
@app.post('/user')
def create_user(request: schemas.User, db: Session = Depends(get_db)):
hashed_password = Hash.bcrypt(request.password)
new_user = models.User(
name=request.name,
email=request.email,
password=hashed_password
)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_userTheory:
- Argon2: Modern password hashing algorithm, more secure than bcrypt
- Why hash? Never store plain text passwords - if database is compromised, user passwords remain safe
- One-way function: You can't reverse a hash back to the original password
Commit: Relationships done
What was done:
- One-to-Many relationship between User and Blog
- Foreign key implementation
Code (models.py):
from sqlalchemy.orm import relationship
class Blog(Base):
__tablename__ = "blogs"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
body = Column(String)
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", back_populates="blogs")
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
email = Column(String)
password = Column(String)
blogs = relationship("Blog", back_populates="user")Code (schemas.py):
class ShowUser(BaseModel):
name: str
email: str
blogs: List[Blog] = []
class Config:
from_attributes = True
class ShowBlog(Blog):
user: Optional[ShowUser] = None
class Config:
from_attributes = TrueTheory:
- Relationships: SQLAlchemy relationships define how tables are connected
- Foreign Key:
user_idreferences theusers.idcolumn - back_populates: Creates bidirectional relationship - access blogs from user and user from blog
- One-to-Many: One user can have many blogs
Commit: Created routers and added tags and refactored main.py
What was done:
- Code organization with APIRouter
- Separation of concerns
- Tagged endpoints for documentation
Files:
routers/blog.pyrouters/user.py
Code (routers/blog.py):
from fastapi import APIRouter
router = APIRouter(
prefix="/blog",
tags=["Blogs"],
)
@router.get('/', response_model=List[schemas.ShowBlog])
def all(db: Session = Depends(database.get_db)):
blogs = db.query(models.Blog).all()
return blogs
@router.post('/')
def create(request: schemas.Blog, db: Session = Depends(database.get_db)):
new_blog = models.Blog(title=request.title, body=request.body, user_id=1)
db.add(new_blog)
db.commit()
db.refresh(new_blog)
return new_blogCode (main.py):
from .routers import blog, user, authentication
app = FastAPI()
app.include_router(blog.router)
app.include_router(user.router)
app.include_router(authentication.router)Theory:
- APIRouter: Allows grouping related endpoints
- Prefix: Applies URL prefix to all routes in the router
- Tags: Organizes endpoints in Swagger documentation
- Modularity: Separates code by domain (blogs, users, auth)
Commit: Repositories to store functions for user and blog
What was done:
- Business logic separation
- Repository pattern implementation
- Cleaner router code
Files:
repository/blog.pyrepository/user.py
Code (repository/blog.py):
from sqlalchemy.orm import Session
from .. import models, schemas
from fastapi import HTTPException, status
def all(db: Session):
blogs = db.query(models.Blog).all()
return blogs
def create(request: schemas.Blog, db: Session):
new_blog = models.Blog(title=request.title, body=request.body, user_id=1)
db.add(new_blog)
db.commit()
db.refresh(new_blog)
return new_blog
def destroy(id, db: Session):
blog = db.query(models.Blog).filter(models.Blog.id == id)
if not blog.first():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Blog with id {id} not found'
)
blog.delete(synchronize_session=False)
db.commit()
return {"detail": "Blog deleted"}
def update(id, request: schemas.Blog, db: Session):
blog = db.query(models.Blog).filter(models.Blog.id == id)
if not blog.first():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Blog with id {id} not found'
)
blog.update({'title': request.title, 'body': request.body})
db.commit()
return {"detail": "Blog updated"}
def show(id, db: Session):
blog = db.query(models.Blog).filter(models.Blog.id == id).first()
if not blog:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Blog with id {id} not found'
)
return blogUsage (routers/blog.py):
from ..repository import blog
@router.get('/')
def all(db: Session = Depends(database.get_db)):
return blog.all(db)
@router.post('/')
def create(request: schemas.Blog, db: Session = Depends(database.get_db)):
return blog.create(request, db)Theory:
- Repository Pattern: Separates data access logic from business logic
- Benefits:
- Easier testing
- Code reusability
- Cleaner routers
- Single responsibility principle
Commit: Authentication done, JWT generation left
What was done:
- Login endpoint creation
- Password verification
Files:
routers/authentication.pyschemas.py(added Login schema)
Code (schemas.py):
class Login(BaseModel):
email: str
password: strCode (routers/authentication.py):
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .. import schemas, database, models
from ..hashing import Hash
router = APIRouter(tags=["Authentication"])
@router.post('/login')
def login(request: schemas.Login, db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.email == request.email).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid Credentials"
)
if not Hash.verify(user.password, request.password):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid Credentials"
)
return {"message": "Login successful"}Theory:
- Authentication: Verifying user identity
- Process:
- User sends email & password
- Find user by email
- Verify password hash
- Return success/failure
Commit: JWT Auth done
What was done:
- JWT token generation and verification
- Complete authentication system with OAuth2
- Protected endpoints requiring authentication
Files:
JWTtoken.py- Token creation and verificationoauth2.py- OAuth2 scheme and user authentication- Updated
routers/authentication.py- Login with token generation - Updated
routers/blog.py- Protected blog endpoints
Code (JWTtoken.py):
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from blog import schemas
SECRET_KEY = "b4e1a0c9c3e54f71a4f8d8f74c52c1f603f2a5bc8cdd4e61b2f283f54d7e92af"
algorithm = "HS256"
access_token_expire_minutes = 30
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=access_token_expire_minutes)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=algorithm)
return encoded_jwt
def verify_access_token(token: str, credentials_exception):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[algorithm])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = schemas.TokenData(email=email)
return token_data
except JWTError:
raise credentials_exceptionCode (oauth2.py):
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from . import JWTtoken
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
async def get_current_user(data: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return JWTtoken.verify_access_token(data, credentials_exception)Code (routers/authentication.py):
from ..JWTtoken import create_access_token
@router.post('/login')
def login(request: schemas.Login, db: Session = Depends(database.get_db)):
user = db.query(models.User).filter(models.User.email == request.email).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid Credentials"
)
if not Hash.verify(user.password, request.password):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid Credentials"
)
access_token = create_access_token(data={"sub": user.email})
return {"access_token": access_token, "token_type": "bearer"}Code (routers/blog.py - Protected endpoints):
from .. import oauth2
@router.get('/', response_model=List[schemas.ShowBlog])
def all(db: Session = Depends(database.get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)):
return blog.all(db)
@router.post('/')
def create(request: schemas.Blog,
db: Session = Depends(database.get_db),
current_user: schemas.User = Depends(oauth2.get_current_user)):
return blog.create(request, db)Code (schemas.py):
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: str | None = NoneTheory:
- JWT (JSON Web Token): Self-contained token containing user information
- Structure: Header.Payload.Signature
- OAuth2PasswordBearer: FastAPI's OAuth2 implementation with password flow
- Benefits:
- Stateless authentication
- No server-side session storage
- Can be verified without database lookup
- Protected endpoints require valid tokens
- Security:
- Token is signed with SECRET_KEY
- Includes expiration time (30 minutes)
- Cannot be tampered with
- Tokens must be included in Authorization header
How it works:
- User logs in with email/password
- Server verifies credentials
- Server generates JWT with user email
- Token returned to client
- Client includes token in Authorization header:
Bearer <token> - Protected endpoints verify token via
get_current_userdependency - Invalid/expired tokens receive 401 Unauthorized
Protected Endpoints: All blog endpoints now require authentication. Users must:
- Login to receive access token
- Include token in requests:
Authorization: Bearer <your_token> - Token is automatically validated before endpoint execution
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /login |
Login and get JWT token | No |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /user |
Create new user | No |
| GET | /user/{id} |
Get user by ID | No |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /blog |
Get all blogs | β Yes |
| POST | /blog |
Create new blog | β Yes |
| GET | /blog/{id} |
Get blog by ID | β Yes |
| PUT | /blog/{id} |
Update blog | β Yes |
| DELETE | /blog/{id} |
Delete blog | β Yes |
{
"id": int, # Primary key
"name": str, # User's name
"email": str, # User's email (unique)
"password": str, # Hashed password
"blogs": [] # Related blogs (relationship)
}{
"id": int, # Primary key
"title": str, # Blog title
"body": str, # Blog content
"user_id": int, # Foreign key to users
"user": {} # Related user (relationship)
}- Algorithm: Argon2
- Library: passlib
- Passwords are hashed before storage
- Plain text passwords never stored
- Algorithm: HS256
- Expiration: 30 minutes
- Library: python-jose
1. Login to get token:
POST /login
{
"email": "user@example.com",
"password": "mypassword"
}Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}2. Use token to access protected endpoints:
In Swagger UI:
- Click the "Authorize" π button at the top
- Enter your access token in the value field
- Click "Authorize"
- All subsequent requests will include the token
With cURL:
curl -X GET "http://127.0.0.1:8001/blog" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."With Postman/Thunder Client:
- Authorization tab β Type: Bearer Token
- Paste your token in the Token field
| Technology | Purpose |
|---|---|
| FastAPI | Web framework |
| SQLAlchemy | ORM |
| SQLite | Database |
| Pydantic | Data validation |
| Passlib | Password hashing |
| Python-JOSE | JWT tokens |
| Uvicorn | ASGI server |
| Argon2 | Password hashing algorithm |
fastapi==0.123.9
uvicorn==0.36.3
sqlalchemy==2.0.36
passlib==1.7.4
python-jose==3.3.0
argon2-cffi==25.1.0
pydantic==2.12.5- Navigate to http://127.0.0.1:8001/docs
- Try out endpoints interactively
- View request/response schemas
Create User:
curl -X POST "http://127.0.0.1:8001/user" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com", "password": "secret123"}'Login:
curl -X POST "http://127.0.0.1:8001/login" \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "password": "secret123"}'Login (Get Token):
curl -X POST "http://127.0.0.1:8001/login" \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "password": "secret123"}'Create Blog (with token):
curl -X POST "http://127.0.0.1:8001/blog" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{"title": "My First Blog", "body": "This is the content"}'Get All Blogs (with token):
curl -X GET "http://127.0.0.1:8001/blog" \
-H "Authorization: Bearer YOUR_TOKEN_HERE"-
FastAPI Fundamentals
- Path parameters & query parameters
- Request body validation with Pydantic
- Dependency injection
- Response models
-
Database Management
- SQLAlchemy ORM setup
- Model relationships (One-to-Many)
- Session management
- Database migrations
-
Security
- Password hashing with Argon2
- JWT token authentication
- Secure credential handling
-
Code Architecture
- Repository pattern
- Router-based organization
- Separation of concerns
- Clean code principles
Solution: Run uvicorn from the parent directory:
cd d:\py\fastapi
uvicorn blog.main:app --reloadSolution: Use Argon2 instead of bcrypt:
pwd_cxt = CryptContext(schemes=["argon2"], deprecated="auto")Solution: Ensure schema field names match model relationship names (use user not creator)
Solution: Delete blog.db and restart server to recreate tables
Solution: All blog endpoints now require authentication. You must:
- Create a user account first
- Login to get an access token
- Include the token in your requests:
Authorization: Bearer <token> - In Swagger UI, click the "Authorize" button and paste your token
This application is deployed on Render.com with a free tier. Here's a complete step-by-step guide to deploy your own instance.
- β Free Tier Available - Perfect for learning and demo projects
- β Automatic Deployments - Deploys on every git push
- β Built-in CI/CD - No extra configuration needed
- β Easy Setup - Deploy in minutes
- β HTTPS by Default - Automatic SSL certificates
- GitHub Account - Your code must be in a GitHub repository
- Render Account - Sign up at render.com (free)
- Working FastAPI Project - Ensure your app runs locally
Ensure your requirements.txt has all dependencies with specific versions:
fastapi==0.123.9
uvicorn[standard]==0.38.0
sqlalchemy==2.0.36
passlib==1.7.4
python-jose==3.3.0
argon2-cffi==25.1.0
pydantic==2.12.5
python-multipart==0.0.20Key Requirements:
uvicorn[standard]- ASGI server with performance extraspython-multipart- Required for OAuth2 form data (login)- Specific versions prevent deployment errors
# Initialize git (if not already done)
git init
git add .
git commit -m "Initial commit"
# Create a repository on GitHub, then:
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git branch -M main
git push -u origin main-
Go to Render Dashboard
- Visit dashboard.render.com
- Click "New +" β "Web Service"
-
Connect GitHub Repository
- Click "Connect account" if not already connected
- Authorize Render to access your repositories
- Select your FastAPI repository
-
Configure Service Settings
Setting Value Name fastapi-blog-api(or your preferred name)Region Choose closest to you Branch mainRoot Directory Leave empty (unless app is in subdirectory) Runtime Python 3Build Command pip install -r requirements.txtStart Command uvicorn blog.main:app --host 0.0.0.0 --port $PORT -
Select Instance Type
- Choose "Free" plan
β οΈ Note: Free tier spins down after 15 minutes of inactivity
-
Advanced Settings (Optional)
- Auto-Deploy: Keep enabled (deploys on every push)
- Environment Variables: Add if needed (see below)
-
Click "Create Web Service"
Render will now:
- Clone your repository
- Install Python 3.12 (or latest stable)
- Run build command:
pip install -r requirements.txt - Start your app:
uvicorn blog.main:app --host 0.0.0.0 --port $PORT
You can watch the logs in real-time:
==> Installing dependencies...
==> Collecting fastapi==0.123.9
==> Successfully installed fastapi-0.123.9 uvicorn-0.38.0...
==> Starting service...
==> INFO: Started server process [1]
==> INFO: Uvicorn running on http://0.0.0.0:10000
Once deployment succeeds:
- API URL:
https://your-service-name.onrender.com - Swagger Docs:
https://your-service-name.onrender.com/docs - ReDoc:
https://your-service-name.onrender.com/redoc
Live Demo: https://fastapi-agrf.onrender.com/docs
uvicorn blog.main:app --host 0.0.0.0 --port $PORTblog.main:app- Path to your FastAPI app instance--host 0.0.0.0- Listen on all network interfaces (required for Render)--port $PORT- Use Render's dynamically assigned port (critical!)--reload- β Do NOT use in production (only for local development)
Render assigns a random port via the $PORT environment variable. Using --port 8001 or hardcoded ports will cause deployment to fail.
For production, you should use environment variables for sensitive data:
In Render Dashboard:
- Go to your service β "Environment" tab
- Add environment variables:
SECRET_KEY=your-super-secret-key-here-generate-a-new-one
DATABASE_URL=sqlite:///./blog.db
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
Update JWTtoken.py to use environment variables:
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY", "fallback-secret-key")
algorithm = os.getenv("ALGORITHM", "HS256")
access_token_expire_minutes = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))Add to requirements.txt:
python-dotenv==1.0.0- β Simple, no extra setup
- β Data is lost on every redeploy (free tier limitation)
- β Ephemeral storage - file system is not persistent
On Render:
-
Create a PostgreSQL database (also has free tier)
-
Copy the Internal Database URL
-
Add to environment variables:
DATABASE_URL=postgresql://user:password@host:5432/database -
Update
requirements.txt:psycopg2-binary==2.9.9
-
Update
database.py:import os SQLALCHEMY_DATABASE_URL = os.getenv( "DATABASE_URL", "sqlite:///./blog.db" # Fallback for local development ) # Handle Render's postgres:// URL format if SQLALCHEMY_DATABASE_URL.startswith("postgres://"): SQLALCHEMY_DATABASE_URL = SQLALCHEMY_DATABASE_URL.replace( "postgres://", "postgresql://", 1 )
Render automatically redeploys on every push to your connected branch:
# Make changes
git add .
git commit -m "Update feature"
git push origin main
# Render detects push and redeploys automatically!Watch deployment logs:
- Go to Render Dashboard β Your Service β "Logs" tab
- See real-time build and deployment progress
Error:
ERROR: Could not find a version that satisfies the requirement uvicorn==0.36.3
Solution:
Update requirements.txt with correct package versions:
uvicorn[standard]==0.38.0Error:
RuntimeError: Form data requires "python-multipart" to be installed
Solution:
Add to requirements.txt:
python-multipart==0.0.20Cause: Free tier services spin down after 15 minutes of inactivity.
Solution:
- First request may take 30-60 seconds to wake up
- Consider upgrading to paid tier for always-on service
- Use a monitoring service to ping your API periodically (e.g., UptimeRobot)
Cause: SQLite data is not persistent on free tier.
Solution:
- Use Render's PostgreSQL database (free tier available)
- Or upgrade to paid tier for persistent disk storage
Error:
Access to fetch at 'https://your-api.onrender.com' from origin 'http://localhost:3000' has been blocked by CORS policy
Solution:
Add CORS middleware in blog/main.py:
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify exact origins
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Render Dashboard provides:
- Logs: Real-time application logs
- Metrics: CPU, Memory, Request stats (paid tiers)
- Events: Deployment history
- Shell Access: Debug via web terminal (paid tiers)
View Logs:
# In Render dashboard
Logs β Live Logs| Feature | Free Tier | Paid Tier |
|---|---|---|
| Sleep after inactivity | β Yes (15 min) | β No |
| Build minutes | 500 min/month | Unlimited |
| Bandwidth | 100 GB/month | Unlimited |
| Custom domains | β Yes | β Yes |
| Persistent disk | β No | β Yes |
| Autoscaling | β No | β Yes |
| Price | Free | From $7/month |
-
requirements.txtwith all dependencies -
uvicorn[standard]andpython-multipartincluded - Code pushed to GitHub
- Render account created
- Web service configured
- Start command:
uvicorn blog.main:app --host 0.0.0.0 --port $PORT - Deployment successful
- API accessible at Render URL
- Swagger docs working
- Environment variables configured (optional)
- PostgreSQL database connected (optional)
- CORS configured if using frontend (optional)
If Render doesn't meet your needs:
- Railway - Similar to Render, $5/month credit
- Fly.io - Global edge deployment, generous free tier
- DigitalOcean App Platform - $5/month minimum
- AWS Lambda - Serverless (requires Mangum adapter)
- Vercel - Serverless (requires API routes setup)
- PythonAnywhere - Beginner-friendly, limited free tier
This project is based on the FastAPI Course by Bitfumes.
Special thanks to Bitfumes for creating an excellent FastAPI tutorial that forms the foundation of this project.
- β
Upgraded to Pydantic v2 (
from_attributesinstead oform_mode) - β Switched from bcrypt to Argon2 for better security
- β Implemented OAuth2 with JWT token verification
- β Protected all blog endpoints with authentication
- β
Added
oauth2.pymodule for authentication middleware - β Fixed import issues and module structure
- β Added comprehensive documentation
- β Improved error handling
- β Enhanced code organization
- β Added detailed commit-by-commit explanations
- β Included troubleshooting guide
This project is for educational purposes. Please refer to the original repository for licensing information.
DevSsChar
- GitHub: @DevSsChar
If you found this project helpful, please give it a βοΈ!
Built with β€οΈ using FastAPI