FastAPI CLI Integration
Introduction
Command Line Interfaces (CLIs) are powerful tools that allow developers to interact with applications directly from the terminal. By integrating a CLI with your FastAPI application, you can automate common tasks, provide developer utilities, and simplify operations like database migrations, environment setup, or application deployment.
In this tutorial, we'll learn how to create a CLI for your FastAPI application using popular Python CLI libraries like Typer (created by the same developer as FastAPI) and Click. You'll discover how to structure your code to maintain separation of concerns while providing powerful command-line capabilities that complement your web API.
Why Add a CLI to Your FastAPI Application?
Before diving into implementation, let's understand why you might want to add CLI capabilities:
- Development Workflow: Run database migrations, generate code, or seed test data
- Application Management: Start/stop services, manage background tasks
- Administration: Create users, manage permissions, or perform backups
- Automation: Schedule tasks or run batch operations
- Debugging: Run health checks or diagnostics
Setting Up Your FastAPI Project with CLI Support
Prerequisites
Make sure you have a FastAPI project ready. If not, you can create a minimal one:
from fastapi import FastAPI
app = FastAPI(title="My API with CLI")
@app.get("/")
def read_root():
return {"message": "Hello World"}
Installing CLI Libraries
We'll use Typer, which is built on top of Click and created by the same developer as FastAPI:
pip install typer[all]
The [all]
installs optional dependencies like shell completion and colored output.
Creating Your First CLI Command
Let's create a simple CLI for our FastAPI application. Start by creating a new file called cli.py
in your project:
import typer
from typing import Optional
app = typer.Typer(help="CLI for FastAPI application")
@app.command()
def run(port: int = 8000, host: str = "127.0.0.1", reload: bool = False):
"""
Run the FastAPI application with uvicorn.
"""
import uvicorn
uvicorn.run("main:app", host=host, port=port, reload=reload)
@app.command()
def version():
"""
Show the application version.
"""
print("FastAPI App v0.1.0")
if __name__ == "__main__":
app()
Now you can run your FastAPI application using this command:
python cli.py run
Or check the version:
python cli.py version
Structuring Your CLI for Larger Applications
As your application grows, you'll want to organize your CLI commands into groups. Here's how to structure it:
import typer
from typing import Optional
app = typer.Typer(help="CLI for FastAPI application")
# Create command groups
db_app = typer.Typer(help="Database operations")
user_app = typer.Typer(help="User management")
app.add_typer(db_app, name="db")
app.add_typer(user_app, name="user")
# Main commands
@app.command()
def run(port: int = 8000, host: str = "127.0.0.1", reload: bool = False):
"""Run the FastAPI application with uvicorn."""
import uvicorn
uvicorn.run("main:app", host=host, port=port, reload=reload)
# Database commands
@db_app.command("migrate")
def db_migrate(revision: str = None):
"""Run database migrations."""
if revision:
print(f"Running migration to revision: {revision}")
else:
print("Running all pending migrations")
@db_app.command("seed")
def db_seed():
"""Seed the database with initial data."""
print("Seeding database with initial data")
# User commands
@user_app.command("create")
def create_user(username: str, admin: bool = False):
"""Create a new user."""
user_type = "admin" if admin else "regular"
print(f"Creating {user_type} user: {username}")
if __name__ == "__main__":
app()
Now you can run commands like:
python cli.py db migrate
python cli.py user create johndoe --admin
Accessing FastAPI App Context from CLI Commands
A common challenge is accessing your FastAPI app's context (like database connections or settings) from CLI commands. Let's solve this with a practical example:
First, let's update our FastAPI application structure:
myapp/
├── __init__.py
├── main.py # FastAPI app
├── cli.py # CLI entry point
├── db/
│ ├── __init__.py
│ └── session.py # Database setup
└── models/
├── __init__.py
└── user.py # User model
Here's how we can access our database from CLI commands:
# In db/session.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# In models/user.py
from sqlalchemy import Boolean, Column, Integer, String
from ..db.session import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_admin = Column(Boolean, default=False)
Now we can update our CLI to interact with the database:
# In cli.py
import typer
import os
from typing import Optional
from sqlalchemy.orm import Session
app = typer.Typer(help="CLI for FastAPI application")
user_app = typer.Typer(help="User management")
app.add_typer(user_app, name="user")
@user_app.command("create")
def create_user(
username: str,
email: str,
password: str,
admin: bool = False
):
"""Create a new user."""
# Import here to avoid circular imports
from myapp.db.session import SessionLocal
from myapp.models.user import User
import hashlib
db = SessionLocal()
try:
# Simple password hashing (in real app, use proper password hashing)
hashed_password = hashlib.sha256(password.encode()).hexdigest()
user = User(
username=username,
email=email,
hashed_password=hashed_password,
is_admin=admin
)
db.add(user)
db.commit()
db.refresh(user)
print(f"User created: {username} (ID: {user.id})")
except Exception as e:
db.rollback()
print(f"Error creating user: {e}")
finally:
db.close()
@user_app.command("list")
def list_users():
"""List all users."""
from myapp.db.session import SessionLocal
from myapp.models.user import User
db = SessionLocal()
try:
users = db.query(User).all()
if not users:
print("No users found")
return
print(f"{'ID':<5} {'USERNAME':<15} {'EMAIL':<25} {'ADMIN'}")
print("-" * 55)
for user in users:
print(f"{user.id:<5} {user.username:<15} {user.email:<25} {user.is_admin}")
finally:
db.close()
if __name__ == "__main__":
app()
Adding Database Initialization and Migration Commands
Let's add commands to initialize our database and run migrations using Alembic:
# In cli.py (add to existing code)
db_app = typer.Typer(help="Database operations")
app.add_typer(db_app, name="db")
@db_app.command("init")
def init_db():
"""Initialize the database tables."""
from myapp.db.session import engine, Base
from myapp.models.user import User # Make sure to import all models
print("Creating database tables...")
Base.metadata.create_all(bind=engine)
print("Database tables created!")
@db_app.command("migrate")
def db_migrate(message: str = ""):
"""Generate a new migration."""
# This assumes you have alembic configured
import subprocess
cmd = ["alembic", "revision", "--autogenerate"]
if message:
cmd.extend(["-m", message])
subprocess.run(cmd)
print("Migration created successfully")
@db_app.command("upgrade")
def db_upgrade(revision: str = "head"):
"""Apply migrations to the database."""
import subprocess
subprocess.run(["alembic", "upgrade", revision])
print(f"Database upgraded to: {revision}")
Making Your CLI Package Installable
To make your CLI tool installable system-wide, update your project's setup.py
:
from setuptools import setup, find_packages
setup(
name="myapp",
version="0.1.0",
packages=find_packages(),
include_package_data=True,
install_requires=[
"fastapi>=0.68.0",
"uvicorn>=0.15.0",
"typer>=0.4.0",
"sqlalchemy>=1.4.0",
],
entry_points="""
[console_scripts]
myapp=myapp.cli:app
""",
)
Now you can install your application with:
pip install -e .
After installation, you can run commands directly:
myapp run
myapp db init
myapp user create admin [email protected] password --admin
Advanced CLI Features
Adding Colorful Output
Enhance user experience with colored output:
import typer
from rich.console import Console
from rich.table import Table
console = Console()
@user_app.command("list")
def list_users():
"""List all users."""
from myapp.db.session import SessionLocal
from myapp.models.user import User
db = SessionLocal()
try:
users = db.query(User).all()
if not users:
console.print("[yellow]No users found[/yellow]")
return
table = Table(title="Users")
table.add_column("ID", style="dim")
table.add_column("Username", style="green")
table.add_column("Email")
table.add_column("Admin", justify="center")
for user in users:
admin_status = "✓" if user.is_admin else "✗"
table.add_row(
str(user.id),
user.username,
user.email,
admin_status
)
console.print(table)
finally:
db.close()
Adding Progress Bars
For long-running operations, add progress bars:
@db_app.command("seed")
def seed_database(count: int = 100):
"""Seed the database with sample data."""
from myapp.db.session import SessionLocal
from myapp.models.user import User
import random
import string
import hashlib
db = SessionLocal()
try:
with console.status("[bold green]Seeding database..."):
for i in range(count):
# Generate random user data
username = f"user_{i}"
email = f"user_{i}@example.com"
password = ''.join(random.choices(string.ascii_letters, k=8))
hashed_password = hashlib.sha256(password.encode()).hexdigest()
user = User(
username=username,
email=email,
hashed_password=hashed_password,
is_admin=False
)
db.add(user)
# Commit in batches to improve performance
if i % 100 == 0:
db.commit()
db.commit()
console.print(f"[bold green]Successfully created {count} sample users![/bold green]")
except Exception as e:
db.rollback()
console.print(f"[bold red]Error seeding database: {e}[/bold red]")
finally:
db.close()
Integration with FastAPI Configuration
To ensure your CLI and FastAPI app use the same configuration:
# In config.py
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "sqlite:///./app.db"
API_V1_STR: str = "/api/v1"
SECRET_KEY: str = "your-secret-key-here"
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
Update your database session to use this configuration:
# In db/session.py
from ..config import settings
SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL
engine = create_engine(SQLALCHEMY_DATABASE_URL)
# Rest of the code as before
Real-world Example: Application Management CLI
Let's create a more comprehensive example for managing your application:
# In cli.py
import typer
import os
import subprocess
from pathlib import Path
from rich.console import Console
from rich.prompt import Confirm
app = typer.Typer(help="FastAPI Application Manager")
console = Console()
# Command groups
db_app = typer.Typer(help="Database operations")
deploy_app = typer.Typer(help="Deployment operations")
config_app = typer.Typer(help="Configuration operations")
app.add_typer(db_app, name="db")
app.add_typer(deploy_app, name="deploy")
app.add_typer(config_app, name="config")
# Main application commands
@app.command()
def run(port: int = 8000, host: str = "127.0.0.1", reload: bool = True):
"""Run the development server."""
import uvicorn
console.print(f"[bold green]Starting development server at http://{host}:{port}[/bold green]")
uvicorn.run("myapp.main:app", host=host, port=port, reload=reload)
@app.command()
def shell():
"""Open an interactive Python shell with application context."""
# Import context here to have it available in the shell
from myapp.db.session import SessionLocal
from myapp.models.user import User
import code
db = SessionLocal()
console.print("[bold green]Starting interactive shell...[/bold green]")
console.print("[yellow]Available variables:[/yellow] db, User")
code.interact(local=locals())
db.close()
# Configuration commands
@config_app.command("init")
def init_config():
"""Initialize configuration files."""
template_env = Path(".env.example")
target_env = Path(".env")
if target_env.exists():
overwrite = Confirm.ask("Configuration file already exists. Overwrite?")
if not overwrite:
console.print("[yellow]Operation cancelled[/yellow]")
return
if template_env.exists():
with open(template_env, "r") as src, open(target_env, "w") as dst:
dst.write(src.read())
console.print("[bold green].env file created from template[/bold green]")
else:
with open(target_env, "w") as f:
f.write("DATABASE_URL=sqlite:///./app.db\n")
f.write("SECRET_KEY=your-secret-key-change-me\n")
f.write("API_V1_STR=/api/v1\n")
console.print("[bold green]Default .env file created[/bold green]")
# Deployment commands
@deploy_app.command("docker")
def build_docker():
"""Build Docker image for the application."""
if not Path("Dockerfile").exists():
console.print("[bold red]Dockerfile not found![/bold red]")
return
tag = typer.prompt("Enter image tag", default="latest")
subprocess.run(["docker", "build", "-t", f"myapp:{tag}", "."])
console.print(f"[bold green]Docker image built: myapp:{tag}[/bold green]")
@deploy_app.command("start")
def start_production():
"""Start the application in production mode."""
workers = os.cpu_count() or 1
subprocess.run([
"gunicorn",
"-k", "uvicorn.workers.UvicornWorker",
"-w", str(workers),
"-b", "0.0.0.0:8000",
"myapp.main:app"
])
if __name__ == "__main__":
app()
Summary
In this tutorial, you've learned how to integrate a CLI with your FastAPI application using Typer. We covered:
- Setting up a basic CLI structure for your FastAPI app
- Organizing commands into logical groups
- Accessing your FastAPI app context from CLI commands
- Database management commands
- Making your CLI installable
- Advanced CLI features like colored output and progress bars
- Integrating with FastAPI configuration
A well-designed CLI companion for your FastAPI application can significantly improve developer productivity and simplify application management. By following the patterns in this tutorial, you can build powerful CLI tools that leverage your existing FastAPI codebase.
Additional Resources
- Typer Documentation
- Click Documentation
- FastAPI Documentation
- Rich Documentation (for beautiful terminal output)
Exercises
- Add a command to generate API documentation as static HTML files
- Create a command to run tests with pytest and show a summary of results
- Implement a command to check the health of your application's dependencies
- Add shell completion for your CLI commands
- Create a command to generate a new FastAPI route template with tests
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)