FastAPI in production · Chapter 3 of 12 · Backend · new chapter every Wednesday night

By the end of this chapter: Lay out routers, services and schemas so the codebase stays navigable.

The problem

Say you are a backend engineer tasked with building a new API that will eventually have around fifty endpoints. You've already completed the first two chapters of this course, where you learned to get a typed, documented API running with FastAPI and use validation as the boundary between the outside world and your code using Pydantic. You've successfully exposed a few endpoints, but as the number of endpoints grows, you're starting to feel overwhelmed by the complexity of your codebase. You're worried that if you don't get the project structure right now, it will become unmaintainable and difficult to navigate.

You've been using a simple structure, with all your endpoints defined in a single main.py file. However, as the number of endpoints increases, this file is becoming increasingly large and unwieldy. You're finding it harder to locate specific endpoints, and the code is starting to feel disorganized. You're also concerned about the lack of separation between your endpoint handlers and the business logic of your application.

For example, your main.py file might look something like this:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class UserSettings(BaseModel): language: str notification_settings: str @app.post("/settings") async def set_settings(settings: UserSettings): # store settings in database return {"status": "ok"} @app.get("/users") async def get_users(): # retrieve users from database return [{"id": 1, "name": "John Doe"}] @app.post("/products") async def create_product(product: dict): # create product in database return {"status": "ok"} 

As you can see, this structure is already starting to become cumbersome, and you can imagine how it will become even more difficult to manage as the number of endpoints increases. You need to find a way to organize your codebase in a way that will allow you to easily locate specific endpoints, separate your endpoint handlers from your business logic, and scale your application to meet the needs of your growing API.

Before you start

Before you begin, make sure you have the following prerequisites:

  • You have completed the first two chapters of this course and have a basic understanding of FastAPI and Pydantic.
  • You have Python 3.8 or later installed on your system.
  • You have the fastapi and pydantic libraries installed.
  • You have a code editor or IDE of your choice.

To verify that you have the correct setup, run the following command in your terminal:

python --version 

This should output the version of Python you have installed. You can also verify that you have the fastapi and pydantic libraries installed by running:

pip list fastapi pydantic 

This should output the versions of these libraries that you have installed.

Organizing your endpoints

To organize your endpoints, you can use a combination of routers and services. Routers are used to group related endpoints together, while services are used to encapsulate the business logic of your application.

For example, you could create a users router that handles all endpoints related to users, and a products router that handles all endpoints related to products. You could then create services that encapsulate the business logic for each of these domains.

from fastapi import FastAPI, APIRouter from pydantic import BaseModel app = FastAPI() class UserSettings(BaseModel): language: str notification_settings: str users_router = APIRouter() @users_router.post("/settings") async def set_settings(settings: UserSettings): # store settings in database return {"status": "ok"} @users_router.get("/users") async def get_users(): # retrieve users from database return [{"id": 1, "name": "John Doe"}] app.include_router(users_router, prefix="/users") products_router = APIRouter() @products_router.post("/products") async def create_product(product: dict): # create product in database return {"status": "ok"} app.include_router(products_router, prefix="/products") 

Defining your services

To define your services, you can create separate modules that encapsulate the business logic of your application. For example, you could create a users service that handles all user-related logic, and a products service that handles all product-related logic.

# users/service.py from pydantic import BaseModel class UserService: def __init__(self, db): self.db = db def get_users(self): # retrieve users from database return [{"id": 1, "name": "John Doe"}] def set_settings(self, settings: dict): # store settings in database pass 

Build it

To build your API, follow these steps:

  1. Create a new directory for your project and navigate to it in your terminal.
  2. Create a new file called main.py and add the following code:
from fastapi import FastAPI, APIRouter from pydantic import BaseModel app = FastAPI() # define your routers and services here 
  1. Create separate modules for each of your services, and define the business logic for each service.
  2. Create separate routers for each of your domains, and define the endpoints for each router.
  3. Use the include_router method to include each router in your main application.

When this breaks

Here are some common issues you may encounter when building your API:

  • Endpoints are not being registered: Make sure you have included each router in your main application using the include_router method.
  • Services are not being instantiated: Make sure you have instantiated each service correctly, and that you are passing the correct dependencies to each service.
  • Business logic is not being executed: Make sure you have defined the correct business logic for each service, and that you are calling the correct methods on each service.

What it costs

Using a combination of routers and services to organize your API has several benefits, including:

  • Improved maintainability: Your codebase is easier to navigate and understand, making it easier to maintain and update.
  • Improved scalability: Your API is more scalable, as you can easily add new endpoints and services as needed.
  • Improved testability: Your API is easier to test, as you can test each service and endpoint independently.

However, this approach also has some costs, including:

  • Increased complexity: Your codebase may be more complex, as you need to manage multiple routers and services.
  • Increased overhead: Your API may have increased overhead, as you need to instantiate and manage multiple services.

In the interview

When asked about your approach to organizing your API, you might say something like:

"I use a combination of routers and services to organize my API. I define separate routers for each domain, and separate services that encapsulate the business logic for each domain. This approach allows me to improve the maintainability, scalability, and testability of my API, while also reducing the complexity of my codebase."

A weak answer might sound like:

"I just use a single file for all my endpoints. It's easier that way."

A strong answer might sound like:

"I use a combination of routers and services to organize my API. I define separate routers for each domain, and separate services that encapsulate the business logic for each domain. This approach allows me to improve the maintainability, scalability, and testability of my API, while also reducing the complexity of my codebase. For example, in my previous project, I had a users router that handled all user-related endpoints, and a products router that handled all product-related endpoints. I also had separate services for each domain, which encapsulated the business logic for each domain. This approach allowed me to easily add new endpoints and services as needed, and made it easier to test and maintain my API."

Your tasks

  1. Create a new API with five endpoints, using a combination of routers and services to organize your codebase.
  2. Define a users service that encapsulates the business logic for user-related endpoints.
  3. Define a products service that encapsulates the business logic for product-related endpoints.
  4. Use the include_router method to include each router in your main application.
  5. Test your API to ensure that all endpoints are working correctly.

Your tasks this week

Do the exercises above before the next chapter. Reading a tutorial and doing
one are different activities and only one of them changes what you can build.

Stuck on any of them? Say so — describe what you tried and what happened:
tell me where you got stuck. I read every one, and the questions
that come back more than twice get answered in the next chapter.

FastAPI in production

Chapter 3 of 12. New chapter every Wednesday night.
Next: Databases: SQLAlchemy sessions and the async question.

· The full syllabus and every chapter so far
· Subscribers also get the condensed notes for this chapter, the running
recap of everything the series has covered, and the extended guidance:
subscribe

Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. Portfolio · LinkedIn · GitHub.

Need this built, reviewed or taught to your team? Get in touch or email amit@devamit.co.in. Available for senior and founding engineering roles, consulting and training, remote worldwide.