Setting Up a FastAPI Project From Scratch
I spent three years building microservices with Flask before switching to FastAPI, and the first thing that hit me was how much boilerplate I had been carrying around for nothing. Pydantic does most of the heavy lifting if you actually understand how it works under the hood. The second thing was async/await, which turns out to be less about writing prettier code and more about not blocking the event loop when your database queries pile up. Start with a clean directory structure. Don't overcomplicate it. You need a main.py, a config.py, and at minimum a routers folder and a models folder. I used to throw everything into one file because it felt faster in the beginning. It wasn't. Once your project hits twelve routes and you're juggling middleware, dependency injection, and CORS issues across three different services, having a flat structure becomes a liability. Install FastAPI and uvicorn. Run the server with uvicorn main:app --reload during development. The reload flag saves you from restarting the process every time you touch a file. In production you remove that flag and probably run behind Gunicorn with multiple workers. That detail matters more than people admit.
What Real-World FastAPI Projects Actually Look Like
A real project is not the todo app you see in the documentation. It has authentication, rate limiting, database migrations, error handling that doesn't crash the whole service, and at least one endpoint that times out because someone called an external API without a timeout parameter. I learned that the hard way. One of my services went down during a spike in traffic because a call to a payment gateway had no timeout set. The event loop starved. Everything else hung until the connection finally returned twenty seconds later. I added a httpx.AsyncClient with a thirty-second timeout and a retry strategy. It hasn't happened since. The gap between tutorial projects and production-grade FastAPI applications is wide enough to drive a truck through. Tutorials teach you CRUD endpoints. They don't teach you about connection pooling, migration strategies, or what happens when your Pydantic model validators get hit with malformed JSON from a client that doesn't read the docs. Building real projects forces you into those problems. That's where the skill sharpens. Start with a project that has at least three entities with relationships. A blog platform with users, posts, and comments. An inventory tracker with products, categories, and suppliers. Something where you actually need to join tables and manage foreign keys. Use PostgreSQL. SQLite works for prototyping but hides a lot of problems you'll face later, like concurrent write handling and migration edge cases.
Here's the part most tutorials skip: dependency injection is not optional. You will write service layers, you will inject your database sessions, and you will realize halfway through that hardcoding a session object in every route handler is a nightmare. FastAPI's Depends() system handles session management, auth checks, and configuration loading in a way that keeps your routes clean. I resisted it at first because it looked like extra indirection. That changed when I had to swap out the database from PostgreSQL to a read replica for analytics endpoints and realized I could do it by changing a single dependency.
Get the Full Details

The Database Layer
Use SQLAlchemy 2.0 style with async sessions. Not the legacy syntax. The async pattern works naturally with FastAPI's event loop. Define your models as ORM classes, not bare dictionaries. Migration tools like Alembic are non-negotiable. I once deployed a schema change manually with raw SQL on a Friday night. It worked. Then I tried to reproduce it on staging and missed a column constraint. Two hours of restoring backups. Never again. One thing that catches people off guard: Pydantic models and SQLAlchemy models are not the same thing, and mixing them up causes bugs that are hard to trace. Your database models handle persistence. Your response models handle serialization. Keep them separate. I use a pattern where I define CreateUserRequest, UserResponse, and DBUser as three distinct classes. Yes, it's more code. No, you won't regret it when you need to hide the hashed password field from API responses or transform timestamps before sending them to the client.
Authentication and Authorization
JWT tokens are standard. OAuth2PasswordBearer is FastAPI's built-in helper for token-based auth. Set up a login endpoint that returns a token, create a dependency that extracts and validates the token from the Authorization header, and protect your routes with it. The tricky part is refresh tokens and token expiration. I built a system where access tokens lasted fifteen minutes and refresh tokens lasted seven days. Each refresh issued a new pair. It handled the common case of users staying logged in without keeping a long-lived token in circulation. Don't store passwords in plain text. Use passlib with bcrypt or argon2. This is not a place to be clever. One of my early projects used MD5 because it was what I knew. A colleague pointed out the issue within ten minutes of code review. We switched immediately. The lesson: security shortcuts always cost more later.
Testing
If you skip tests, your project will have gaps. Use pytest with httpx for integration testing. Test your endpoints against a test database, not production. Use Depends overrides in your test fixtures to swap the real database session for an in-memory one. This pattern took me a while to get right because the fixture setup is slightly verbose, but once it works, it saves you from deploying broken code. I found that testing async endpoints requires pytest-asyncio. Without it, your async tests just hang. That's a common trap. The error message isn't helpful either. It just looks like the test is running forever. I spent two hours debugging what I thought was a database deadlock before realizing the test runner wasn't actually executing the async code.
Deployment and Production Concerns
Dockerize your application. Write a Dockerfile that uses a slim Python image and installs your dependencies in a virtual environment. Multi-stage builds help keep the image size down. I went from a 1.2GB image to about 180MB by switching to python:3.11-slim and optimizing the install steps. Use Gunicorn with uvicorn workers in production. Not uvicorn alone. Uvicorn's dev server is fine for local work but isn't designed for production load. Gunicorn manages worker processes and handles graceful reloads. Configure the worker count based on your CPU cores. A common formula is (2 x core_count) + 1, but that's a starting point, not a rule. Monitor your actual usage and adjust. Set up proper logging. The default FastAPI logger prints to stdout, which is fine for Docker containers but inadequate for debugging in production. I started using loguru because it handles rotation, formatting, and structured output without much configuration. It also caught an issue for me once where a background task was silently failing. The standard logger would have swallowed that error.
Common Pitfalls
One issue I see constantly: people forget that coroutines in FastAPI run sequentially by default unless you explicitly use background tasks. If you have a slow operation like sending an email or processing an image, don't block the endpoint. Use BackgroundTasks or asyncio.create_task(). A friend of mine had a video transcoding endpoint that took forty seconds to complete. Every request during that time blocked the entire worker. He moved the transcoding to a Celery task queue and the response time dropped to under two hundred milliseconds. Another pitfall: global state in async code. Python's async event loop shares the global namespace, and mutable objects passed around between handlers can cause race conditions. I learned this when my rate limiter started returning incorrect counts during concurrent requests. The fix was using redis for distributed state instead of an in-memory counter.
What This Actually Gets You
Working through these projects gives you something tutorials can't: the ability to debug when things go wrong. You'll encounter import errors, dependency conflicts, database lockups, and middleware quirks. You'll learn to read stack traces instead of panicking. You'll develop a sense for which problems are configuration issues and which are design flaws. That's the real skill gain. Not knowing FastAPI's API by heart. Knowing how to fix it when it breaks at 2 AM. Build a project. Break it. Fix it. Repeat. The cycle is tedious but effective. I've seen too many people jump from tutorial to tutorial without ever shipping anything. There's no substitute for actually deploying code and watching it fail in ways you didn't expect.