Introduction to Python Web Frameworks
Python offers a rich ecosystem of web frameworks, each designed with different philosophies and use cases in mind. Flask, Django, and FastAPI represent three distinct approaches to building web applications—from minimalist microframeworks to full-featured batteries-included solutions, to high-performance async frameworks. Understanding their differences is essential for choosing the right tool for your project.
What Is Flask?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Flask is a lightweight WSGI microframework created by Armin Ronacher. It provides the essentials for building web applications—routing, templating, and request handling—without imposing a specific project structure or including database abstraction layers by default. Flask follows the principle of simplicity and extensibility, allowing developers to add only what they need through a vast ecosystem of extensions.
Core Features of Flask
- Minimalist core: Ships with routing, Jinja2 templating, and a development server
- Extension system: Flask-SQLAlchemy, Flask-WTF, Flask-Login, and hundreds more
- Werkzeug-powered: Robust WSGI toolkit handling request/response objects
- Flexible structure: No enforced project layout or boilerplate
- Synchronous by default: Runs on WSGI servers like Gunicorn
Flask Code Example
# app.py
from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
db = SQLAlchemy(app)
class Task(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
completed = db.Column(db.Boolean, default=False)
@app.route('/')
def index():
tasks = Task.query.all()
return render_template('index.html', tasks=tasks)
@app.route('/api/tasks', methods=['GET', 'POST'])
def tasks_api():
if request.method == 'POST':
data = request.get_json()
task = Task(title=data['title'])
db.session.add(task)
db.session.commit()
return jsonify({'id': task.id, 'title': task.title}), 201
tasks = Task.query.all()
return jsonify([{'id': t.id, 'title': t.title, 'completed': t.completed} for t in tasks])
@app.route('/api/tasks/', methods=['PUT', 'DELETE'])
def task_detail(task_id):
task = Task.query.get_or_404(task_id)
if request.method == 'PUT':
data = request.get_json()
task.completed = data.get('completed', task.completed)
db.session.commit()
return jsonify({'id': task.id, 'completed': task.completed})
db.session.delete(task)
db.session.commit()
return '', 204
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Flask Project Structure (Typical)
project/
├── app.py # Main application entry point
├── templates/
│ └── index.html # Jinja2 templates
├── static/
│ ├── css/
│ └── js/
├── requirements.txt
└── venv/ # Virtual environment
What Is Django?
Django is a high-level, "batteries-included" web framework built for rapid development and clean, pragmatic design. Created in 2003 by Adrian Holovaty and Simon Willison, Django follows the Model-View-Template (MVT) architectural pattern and includes an ORM, admin interface, authentication system, form handling, and much more right out of the box. It enforces a structured project layout and promotes DRY (Don't Repeat Yourself) principles.
Core Features of Django
- ORM: Powerful object-relational mapper with migration support
- Admin interface: Auto-generated administrative dashboard
- Authentication: Built-in user management, permissions, and sessions
- Form handling: Django Forms with validation and CSRF protection
- Template engine: Django Template Language (DTL) with inheritance
- Class-based views: Reusable generic views for common patterns
- Testing framework: Integrated test runner with client and fixtures
Django Code Example
# models.py
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
title = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
# serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['id', 'title', 'description', 'completed', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at']
# views.py
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Task
from .serializers import TaskSerializer
class TaskViewSet(viewsets.ModelViewSet):
serializer_class = TaskSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Task.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
@action(detail=True, methods=['post'])
def toggle_complete(self, request, pk=None):
task = self.get_object()
task.completed = not task.completed
task.save()
return Response({'completed': task.completed})
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='task')
urlpatterns = [
path('api/', include(router.urls)),
]
Django Project Structure
myproject/
├── manage.py # CLI management script
├── myproject/
│ ├── __init__.py
│ ├── settings.py # Project configuration
│ ├── urls.py # Root URL configuration
│ ├── asgi.py # ASGI entry point
│ └── wsgi.py # WSGI entry point
├── tasks/
│ ├── __init__.py
│ ├── models.py # Database models
│ ├── views.py # View logic
│ ├── serializers.py # DRF serializers
│ ├── urls.py # App URL routing
│ ├── admin.py # Admin configuration
│ ├── tests.py # Test cases
│ └── migrations/ # Database migrations
├── templates/
│ └── base.html
└── static/
├── css/
└── js/
What Is FastAPI?
FastAPI is a modern, high-performance web framework for building APIs with Python, created by Sebastián Ramírez. It leverages Python 3.6+ type hints and Starlette for async capabilities, combined with Pydantic for data validation and serialization. FastAPI is designed specifically for API development, offering automatic OpenAPI/Swagger documentation generation, asynchronous support, and exceptional performance that rivals Node.js and Go frameworks.
Core Features of FastAPI
- Automatic docs: Interactive Swagger UI and ReDoc generated from type hints
- Async-first: Built on Starlette's ASGI server, supporting async/await
- Pydantic validation: Request and response models with automatic validation
- Dependency injection: Clean, reusable dependency system
- WebSocket support: Native WebSocket handling alongside HTTP
- Background tasks: Built-in support for post-response processing
- Type safety: Full editor autocompletion and linting support
FastAPI Code Example
# main.py
from fastapi import FastAPI, Depends, HTTPException, status, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session, relationship
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
# --- Database Setup ---
SQLALCHEMY_DATABASE_URL = "sqlite:///./tasks.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class TaskModel(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
priority = Column(Integer, default=1)
created_at = Column(DateTime, default=datetime.utcnow)
Base.metadata.create_all(bind=engine)
# --- Pydantic Schemas ---
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
priority: int = Field(default=1, ge=1, le=5)
class TaskUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = None
completed: Optional[bool] = None
priority: Optional[int] = Field(None, ge=1, le=5)
class TaskResponse(BaseModel):
id: int
title: str
description: Optional[str]
completed: bool
priority: int
created_at: datetime
class Config:
orm_mode = True
# --- FastAPI App ---
app = FastAPI(
title="Task Manager API",
description="A high-performance task management API built with FastAPI",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --- Dependency Injection ---
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# --- Routes ---
@app.get("/", tags=["Root"])
async def root():
return {"message": "Task Manager API", "docs": "/docs", "redoc": "/redoc"}
@app.get("/tasks", response_model=List[TaskResponse], tags=["Tasks"])
async def list_tasks(
skip: int = 0,
limit: int = 100,
completed: Optional[bool] = None,
db: Session = Depends(get_db)
):
query = db.query(TaskModel)
if completed is not None:
query = query.filter(TaskModel.completed == completed)
tasks = query.offset(skip).limit(limit).all()
return tasks
@app.post("/tasks", response_model=TaskResponse, status_code=status.HTTP_201_CREATED, tags=["Tasks"])
async def create_task(
task: TaskCreate,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db)
):
db_task = TaskModel(**task.dict())
db.add(db_task)
db.commit()
db.refresh(db_task)
# Simulate background notification
background_tasks.add_task(send_notification, db_task.id)
return db_task
@app.get("/tasks/{task_id}", response_model=TaskResponse, tags=["Tasks"])
async def get_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.put("/tasks/{task_id}", response_model=TaskResponse, tags=["Tasks"])
async def update_task(
task_id: int,
task_update: TaskUpdate,
db: Session = Depends(get_db)
):
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
update_data = task_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(task, field, value)
db.commit()
db.refresh(task)
return task
@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Tasks"])
async def delete_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(TaskModel).filter(TaskModel.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
db.delete(task)
db.commit()
return None
# --- WebSocket Example ---
from fastapi import WebSocket, WebSocketDisconnect
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"Client says: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast("A client disconnected")
# --- Background Task ---
async def send_notification(task_id: int):
# Simulate async notification logic
await asyncio.sleep(0.5)
print(f"Notification sent for task {task_id}")
# For running with uvicorn
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
Detailed Framework Comparison
1. Philosophy and Design Approach
Flask embraces a minimalist philosophy—it gives you the essentials and gets out of your way. You build your application exactly as you envision it, choosing your own ORM, authentication library, and project structure. Django takes the opposite approach: it provides a complete, opinionated framework where components work seamlessly together. FastAPI sits between them conceptually but targets a specific niche—API development with modern Python features and async capabilities.
2. Performance Characteristics
FastAPI consistently outperforms both Flask and Django in benchmarks due to its async architecture and Starlette foundation. Here's a comparison using a simple JSON endpoint with 10,000 concurrent requests:
# Benchmark comparison (requests per second, approximate)
# Environment: 4 CPU cores, 8GB RAM, Python 3.11
Flask (Gunicorn sync workers, 4 processes):
# ~1,200 req/s - JSON endpoint with DB query
Django (Gunicorn sync workers, 4 processes):
# ~950 req/s - JSON endpoint with ORM query
FastAPI (Uvicorn with uvloop, 4 workers):
# ~3,800 req/s - JSON endpoint with async DB query
# The performance gap widens significantly under high concurrency
# FastAPI's async model shines with I/O-bound operations
3. Learning Curve
Flask has the gentlest learning curve—you can build a working app in minutes. Django requires more initial investment due to its many components and conventions, but pays dividends on larger projects. FastAPI requires understanding async programming and type hints, making it slightly more challenging for beginners but rewarding for experienced developers.
4. Database and ORM Integration
# Flask: Manual ORM setup with SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# You configure everything manually
# Django: Built-in ORM with automatic migration detection
python manage.py makemigrations
python manage.py migrate
# Models automatically sync with database schema
# FastAPI: Async ORM with SQLAlchemy 1.4+/2.0 or Tortoise ORM
from sqlalchemy.ext.asyncio import AsyncSession
# Requires async session management
5. API Development and Documentation
This is where FastAPI truly excels. It generates interactive API documentation automatically from your type annotations and Pydantic models:
# FastAPI auto-generates:
# - Swagger UI at /docs
# - ReDoc at /redoc
# - OpenAPI JSON schema at /openapi.json
# Flask requires extensions for similar functionality:
# pip install flask-swagger-ui flasgger
# Manual schema definition or decorator-based docs
# Django with DRF provides:
# - Browsable API (HTML interface for API testing)
# - OpenAPI schema generation with drf-spectacular
# - Less automatic than FastAPI's type-hint-driven approach
6. Async Support
Flask is fundamentally synchronous (WSGI) and cannot natively handle async/await. While Quart exists as an async Flask-compatible alternative, Flask itself remains synchronous. Django added async support gradually—async views, async ORM queries, and ASGI support exist but are not fully async throughout the stack. FastAPI is async-native from the ground up, built on ASGI, and designed for concurrent I/O operations.
# Flask: Synchronous only
@app.route('/sync-endpoint')
def sync_handler():
# Cannot use await here
result = requests.get('https://api.example.com') # Blocking call
return {'data': result.json()}
# Django: Partial async support
async def async_view(request):
# Async view works, but ORM queries may be synchronous
tasks = await sync_to_async(list)(Task.objects.all())
return JsonResponse({'tasks': tasks})
# FastAPI: Fully async
@app.get('/async-endpoint')
async def async_handler():
async with httpx.AsyncClient() as client:
response = await client.get('https://api.example.com') # Non-blocking
return {'data': response.json()}
When to Use Each Framework
Choose Flask When:
- Building microservices or small to medium applications
- You need maximum flexibility in project structure
- Your team prefers minimal boilerplate
- Building REST APIs with traditional sync patterns
- Prototyping or building simple web interfaces
- You want to incrementally add features via extensions
Choose Django When:
- Building large, complex web applications with many features
- You need a robust admin interface out of the box
- Working with content-heavy websites (CMS, blogs, e-commerce)
- Your team benefits from Django's conventions and structure
- You need built-in authentication, sessions, and security features
- Rapid development with "batteries included" is a priority
Choose FastAPI When:
- Building high-performance APIs or microservices
- You need automatic API documentation (OpenAPI/Swagger)
- Handling WebSocket connections alongside HTTP
- Building async applications with many concurrent I/O operations
- You want strong type safety and editor autocompletion
- Building real-time applications or data streaming services
- Machine learning model serving with request validation
Best Practices Across Frameworks
Flask Best Practices
# 1. Use application factories for flexibility
def create_app(config_name='development'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# Initialize extensions
db.init_app(app)
migrate.init_app(app)
# Register blueprints
from .api import api_bp
app.register_blueprint(api_bp, url_prefix='/api')
return app
# 2. Organize with blueprints for modularity
# api/tasks.py
from flask import Blueprint
tasks_bp = Blueprint('tasks', __name__)
@tasks_bp.route('/tasks')
def list_tasks():
return jsonify([])
# 3. Use environment variables for configuration
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY')
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# 4. Implement proper error handling
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Resource not found'}), 404
# 5. Use Flask-CORS for API security
from flask_cors import CORS
CORS(app, origins=['https://myfrontend.com'])
Django Best Practices
# 1. Keep business logic in models or service layer
class Task(models.Model):
# ...
def mark_complete(self):
self.completed = True
self.completed_at = timezone.now()
self.save()
# Trigger signals or notifications here
@classmethod
def get_overdue_tasks(cls, user):
return cls.objects.filter(
user=user,
due_date__lt=timezone.now(),
completed=False
)
# 2. Use Django Debug Toolbar in development
# settings.py
if DEBUG:
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
# 3. Optimize queries with select_related and prefetch_related
tasks = Task.objects.select_related('user').prefetch_related('tags').all()
# 4. Use environment-specific settings
# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = ['myapp.com']
# 5. Implement custom middleware for cross-cutting concerns
class RequestTimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
start = time.time()
response = self.get_response(request)
duration = time.time() - start
response['X-Request-Duration'] = str(duration)
return response
FastAPI Best Practices
# 1. Use dependency injection for shared logic
from fastapi import Depends, Header, HTTPException
async def get_current_user(
authorization: str = Header(...),
db: Session = Depends(get_db)
):
# Token validation logic
user = verify_token(authorization)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
return user
@app.get("/protected")
async def protected_route(user = Depends(get_current_user)):
return {"user": user.username}
# 2. Structure with routers for modular APIs
# routers/tasks.py
from fastapi import APIRouter
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
async def list_tasks():
return []
# main.py
app.include_router(tasks.router)
app.include_router(users.router, prefix="/users")
# 3. Use Pydantic for comprehensive validation
from pydantic import BaseModel, EmailStr, validator
class UserCreate(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
@validator('password')
def password_strength(cls, v):
if not any(char.isupper() for char in v):
raise ValueError('Password must contain uppercase letter')
return v
# 4. Implement proper async database sessions
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
async_engine = create_async_engine("postgresql+asyncpg://...")
async def get_async_db():
async with AsyncSession(async_engine) as session:
yield session
# 5. Use middleware for logging and monitoring
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
logger.info(f"{request.method} {request.url.path} - {duration:.3f}s")
return response
Testing Approaches Comparison
# Flask Testing
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app('testing')
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
def test_create_task(client):
response = client.post('/api/tasks', json={'title': 'Test task'})
assert response.status_code == 201
# Django Testing
from django.test import TestCase, Client
from .models import Task
class TaskTestCase(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('test', password='pass')
def test_create_task(self):
self.client.login(username='test', password='pass')
response = self.client.post('/api/tasks/', {'title': 'Test'})
self.assertEqual(response.status_code, 201)
# FastAPI Testing
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_create_task():
response = client.post("/tasks", json={"title": "Test task", "priority": 2})
assert response.status_code == 201
data = response.json()
assert data["title"] == "Test task"
assert "id" in data
Deployment Considerations
Each framework has different deployment patterns. Flask applications typically run behind Gunicorn with multiple sync workers. Django supports both WSGI (Gunicorn) and ASGI (Daphne/Uvicorn) deployment, with the latter needed for async views and WebSockets. FastAPI runs on ASGI servers like Uvicorn, often behind Nginx as a reverse proxy, and benefits significantly from uvloop for event loop performance.
# Flask deployment with Gunicorn
# gunicorn_config.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
timeout = 120
# Run: gunicorn -c gunicorn_config.py app:app
# Django deployment with Gunicorn + ASGI
# For WSGI: gunicorn myproject.wsgi:application --workers 4
# For ASGI: uvicorn myproject.asgi:application --workers 4 --loop uvloop
# FastAPI deployment with Uvicorn
# Run: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --loop uvloop
# Production Dockerfile example for FastAPI
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Security Features Comparison
Django provides the most comprehensive built-in security features—CSRF protection, XSS filtering, clickjacking defense, SQL injection protection via ORM, and secure session management are all enabled by default. Flask provides basic security through Werkzeug and Jinja2's autoescaping, but requires extensions for CSRF protection and other features. FastAPI relies on Starlette's security middleware and encourages explicit validation through Pydantic, which prevents many injection attacks automatically.
# Django: Security built-in
# settings.py
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
X_FRAME_OPTIONS = 'DENY'
# Flask: Requires explicit setup
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
@app.after_request
def set_security_headers(response):
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
return response
# FastAPI: Middleware-based security
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(HTTPSRedirectMiddleware)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["myapp.com"])
Community and Ecosystem
Django boasts the largest and most mature ecosystem with thousands of third-party packages, extensive documentation, and a massive community. Flask has a robust extension ecosystem and widespread adoption in startups and enterprise environments. FastAPI, while newer, has experienced explosive growth and now has a vibrant community with rapidly expanding third-party tooling, though its ecosystem is still maturing compared to the other two frameworks.
Conclusion
Flask, Django, and FastAPI each serve distinct purposes in the Python web development landscape. Flask excels in simplicity and flexibility, making it ideal for microservices, prototypes, and applications where you want complete control over your stack. Django shines as a full-featured framework for complex, content-driven applications that benefit from its batteries-included approach, robust ORM, and admin interface. FastAPI represents the modern evolution of Python web frameworks, delivering exceptional performance, automatic documentation, and async-native capabilities that make it the premier choice for high-throughput APIs, real-time applications, and microservices in 2024 and beyond.
The choice ultimately depends on your project requirements, team expertise, and long-term maintenance considerations. Many organizations successfully use multiple frameworks within their technology stack—Django for the main application, FastAPI for high-performance microservices, and Flask for simple internal tools. Understanding the strengths and tradeoffs of each framework empowers you to make informed architectural decisions that align with your project's goals.