Pular para o conteúdo
← Back to Skalablog

Published article

FastAPI Project Structure: Routes, Validators, Server

Software EngineeringOpenAI

A clean FastAPI project structure separates the server instance, the route modules, and the Pydantic validators into their own folders under src. FastAPI is a Python web framework that builds request validation and OpenAI documentation from type hints. The layout below keeps that generation predictable as endpoints accumulate.

What a clean FastAPI project structure looks like

A clean FastAPI project structure puts the app instance, the route modules, and the Pydantic validators in separate folders under src, so each file has one reason to change. Splitting those responsibilities early keeps the generated docs readable as endpoints accumulate.

The Rocketseat walkthrough published on 2026-03-31 uses three folders inside src/main:

  • server holds the FastAPI instance and application-level configuration
  • routes holds the API routers
  • validators holds the Pydantic models that describe request bodies

A run.py file at the project root starts Uvicorn. Everything else, including the virtual environment and requirements.txt, stays outside the code folder.

That split is not a framework requirement. FastAPI accepts a single main.py with every route inline, and for a script with two endpoints that is fine. The separation earns its place when the project has more than one resource, when middleware configuration changes independently of routing, or when more than one person edits the file.

The full directory tree after the walkthrough is complete:

project/
├── .vscode/
├── venv/
├── run.py
├── requirements.txt
└── src/
    ├── __init__.py
    └── main/
        ├── __init__.py
        ├── server/
        │   ├── __init__.py
        │   └── server.py
        ├── routes/
        │   ├── __init__.py
        │   └── users_routes.py
        └── validators/
            ├── __init__.py
            └── user_register_validator.py

Python virtual environment and dependency setup

Create the virtual environment before installing anything: python3 -m venv venv, then activate it with source venv/bin/activate on Linux or macOS. A virtual environment isolates project dependencies from the system Python installation, which prevents version conflicts between projects on the same machine and keeps pip freeze output meaningful.

On Windows the activation command differs from the Linux path. The transcript's speaker works on Linux and notes the difference, so check the Python venv documentation for the exact activation script on your platform before assuming one command works everywhere.

The setup order matters:

  1. Create the environment with python3 -m venv venv.
  2. Activate it with source venv/bin/activate on Linux or macOS.
  3. Install the framework with pip install fastapi from the FastAPI package page on PyPI.
  4. Freeze the resolved versions with pip freeze > requirements.txt.
  5. Install the ASGI server with pip install uvicorn and freeze again so requirements.txt stays current.
  6. Point the editor at the project interpreter.

The pip install fastapi command pulls Pydantic in as a transitive dependency, which is why requirements.txt lists it after the first freeze. Tying the app and the server into one document makes the environment reproducible: pip install -r requirements.txt rebuilds exactly that set on another machine.

Point the editor at the project interpreter after activation. In VS Code, the interpreter picker lists every Python on the machine, and the correct choice is the one whose path contains the project's venv folder. The walkthrough uses Python 3.12. Selecting the wrong interpreter is the usual reason imports resolve in the terminal but not in the editor.

The walkthrough also carries a .vscode/settings.json that hides compiled artifacts such as __pycache__, .pyc files, and test cache folders so they do not clutter the file explorer.

Splitting server, routes, and validators

Each folder inside src/main owns exactly one layer: server creates the FastAPI instance, routes declares endpoints on an APIRouter, and validators defines the Pydantic models that type the request body. Adding an empty __init__.py to each folder makes them importable packages and simplifies the import paths between them.

The server.py module imports FastAPI and assigns the instance: from fastapi import FastAPI followed by app = FastAPI(). Middleware configuration, such as CORS, belongs in this file because it applies to the whole application rather than to one router. FastAPI's CORS documentation covers the CORSMiddleware import path and the allow_credentials option.

A typical CORS block in server.py looks like this:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
)

allow_origins lists the origins the browser is allowed to call. allow_credentials=True permits cookies and authorization headers on those cross-origin requests, and it only makes sense when the origin list is restricted. In the walkthrough the CORS block is commented out because the local demo does not need it, which also demonstrates the reload behavior described below.

Route modules declare their own router with a tag, then register it on the app in server.py through app.include_router(). Tags group endpoints in the generated documentation, and the tag string is what appears as the section label in Swagger UI.

Validators live in their own folder because they are reusable. The same UserRegisterValidator model can type a request body, appear in a response schema, and be imported by tests without pulling in the route module. The walkthrough places validators inside main, and notes the folder works equally well at the src level.

Defining the request body with Pydantic

Pydantic, the validation library FastAPI integrates with, converts a class of typed attributes into a request schema. The walkthrough imports BaseModel and Field from the Pydantic documentation, then declares a UserRegisterValidator class:

from pydantic import BaseModel, Field

class UserRegisterValidator(BaseModel):
    username: str = Field(min_length=3)
    age: int = Field(gt=0)
    uf: str

Field adds constraints that plain type annotations cannot express. Field(min_length=3) on the username enforces a three-character floor, and Field(gt=0) on the age rejects zero and negative values. FastAPI turns those constraints into validation errors with HTTP 422 responses and lists the failing field in the response body.

A body shaped like this validator in other stacks is often called a DTO or a domain object. In FastAPI it is the Pydantic model itself, which is why the same class can type the request, the response, and the tests.

FastAPI also reads the model to build the example payload in the docs, which is why the walkthrough's /docs page shows username, age, and uf fields pre-filled for the try-it-out form. The schema follows the OpenAI Specification, the same format other tools consume for client generation.

Wiring the POST route and running Uvicorn

The route module creates a router, decorates an async function with @users_routes.post('/users', status_code=201), and takes the Pydantic model as its body parameter. FastAPI injects and validates that parameter from the JSON payload, so the function body only handles the success path.

The minimal users_routes.py after the walkthrough:

from fastapi import APIRouter

from src.main.validators.user_register_validator import UserRegisterValidator

users_routes = APIRouter(tags=['usuários'])

@users_routes.post('/users', status_code=201)
async def create_user(body: UserRegisterValidator):
    return {'message': 'usuário criado com sucesso', 'attributes': body.dict()}

body.dict() converts the validated model back into a dictionary for the response. Pydantic also exposes model_dump(), the non-deprecated name for the same operation, which is worth preferring in new code.

Back in server.py, importing the router and calling app.include_router(users_routes) registers every endpoint the module declares. This is the single line that connects the routing layer to the application, and it means adding a new route module requires one import and one call.

Uvicorn, the ASGI server FastAPI applications run on, starts the app with uvicorn.run('src.main.server.server:app', host='0.0.0.0', port=3001, reload=True). The dotted path before the colon locates the module and the name after the colon is the FastAPI instance. The Uvicorn documentation describes the same import-string form for the command line.

The run.py wrapper is small:

import uvicorn

if __name__ == '__main__':
    uvicorn.run('src.main.server.server:app', host='0.0.0.0', port=3001, reload=True)

The host='0.0.0.0' value binds the server to every network interface on the machine, so other devices on the network can reach it, not just localhost. reload=True watches the source files and restarts the server when one changes. In the walkthrough, commenting out a broken middleware block and saving the file is enough to bring the server back without stopping the process. Reload mode is for development, not for production deployments.

Returning a dictionary from the route produces a JSON response. A status_code=201 argument on the decorator sets the Created status, and the response body in the walkthrough carries a message string plus the submitted attributes.

FastAPI vs Flask and Django for a new project

FastAPI, Flask, and Django all serve Python web applications, but they differ in what they provide out of the box and in how they handle request validation. The comparison below covers the dimensions that matter when starting a typed JSON API in 2026.

FrameworkRequest validationInteractive docsBuilt-in ORMBest fit
FastAPIFrom Pydantic type hintsSwagger UI at /docs, generated from codeNoTyped JSON APIs that other services call
FlaskAdd-on libraries such as marshmallow or pydanticSeparate flask-swagger or manual specNoSmall services with flexible structure
DjangoDRF serializersDRF schema plus a separate UI packageYes, Django ORMFull applications with admin and templates

FastAPI generates interactive documentation from type hints with no extra configuration. That is the practical difference a developer notices first, and it is why the walkthrough's /docs page works after a single decorator. Django's answer is heavier: serializers, a schema tool, and a separate UI layer, which pays off when the project also needs the admin, authentication, and an ORM. Flask leaves the choice to you, which is freedom when the API is one file and overhead when it is twenty.

What the generated Swagger UI shows

Visiting http://localhost:3001/docs renders the Swagger UI that FastAPI serves automatically, with one collapsible section per router tag and a try-it-out form for each endpoint. Swagger UI is the interactive documentation interface bundled with FastAPI through its OpenAI schema, and it reflects the route decorators and Pydantic models without any hand-written YAML.

The try-it-out form pre-fills the request body from the Pydantic model, so submitting Rafa, 30, and SP returns a 201 response with the message from the route handler. The response panel also shows the raw curl command, which is useful for reproducing a request outside the browser.

Because the docs come from the code, a change to the validator's constraints appears in the docs on the next reload. There is no separate specification file to keep in sync, which is the main reason teams adopt FastAPI for JSON APIs that other services call.

Common errors and how to read them

The errors the walkthrough hits are typical of every first FastAPI project, and each one points at a specific layer:

  • HTTP 422 on a POST request means the body failed validation. FastAPI lists the offending field and the constraint it broke, so Field(gt=0) firing on age shows up as a 422 with age named in the response.
  • A missing middleware package raises an import error on from fastapi.middleware.cors import CORSMiddleware when the middleware dependency is not installed. The walkthrough's CORS block fails first for this reason, which is why it is commented out.
  • Module not found for the app path means the dotted string in uvicorn.run does not match the file layout. src.main.server.server:app only resolves if every folder along the way has an __init__.py, which is the easiest check to run first.
  • Reload saves the process without restarting when the changed file lives outside the watched directory. Keeping code under src avoids that.

Fixing the CORS block in the walkthrough also demonstrates reload: comment the broken lines out, save, and Uvicorn restarts on its own, without a manual stop and start.

FAQ

What is the best folder structure for a FastAPI project?

A common layout puts src/main/server for the app instance, src/main/routes for routers, and src/main/validators for Pydantic models, with a run.py at the root to start Uvicorn. Each folder holds one responsibility, which keeps imports short and files small as the API grows.

Do I need a virtual environment for FastAPI?

Yes, and the reason is version isolation rather than framework requirements. A venv keeps the project's FastAPI and Pydantic versions apart from the system Python and any other project, so pip freeze > requirements.txt records exactly what that application needs.

What is Uvicorn used for in a FastAPI project?

Uvicorn is the ASGI server that runs the FastAPI application. FastAPI defines the app and its routes, and Uvicorn handles the HTTP connection, the event loop, and the development reload behavior when reload=True is passed.

Why separate routes from validators?

Routes describe HTTP behavior, such as the path, method, and status code, while validators describe data shape and constraints. Keeping them apart lets the same Pydantic model serve multiple endpoints and tests, and it stops route files from growing into mixed concerns.

Does FastAPI generate documentation automatically?

FastAPI builds an OpenAI schema from the type hints in the routes and Pydantic models, then serves an interactive Swagger UI at /docs. The documentation updates when the code reloads, with no separate specification file to maintain by hand.

Is allow_credentials=True required for CORS?

Only when the browser must send credentials such as cookies or authorization headers to a different origin. Setting it to true while leaving allowed origins wide open is a common misconfiguration, so restrict allow_origins before enabling credentials.

Can validators live outside the main folder?

Yes. Placing validators inside src/main keeps the application layers together, while placing it beside main under src works just as well and suits projects that share models across more than one application. Pick one location and keep it consistent.

What Python version does FastAPI support?

Modern FastAPI releases run on supported Python releases including 3.12, which is the version used in the Rocketseat walkthrough. Check the classifier list on the FastAPI PyPI page before pinning a version in requirements.txt.

Why does my request return 422 instead of 201?

A 422 means the request body failed Pydantic validation: a too-short username, a zero or negative age, or a missing field. The response names the field and the failed constraint, so the fix is either a correct payload or a validator change. A 201 means the body validated and the route returned its dictionary.

What is a DTO and does FastAPI use them?

DTO stands for data transfer object, a class that defines the shape of data moving between a client and the server. In FastAPI the Pydantic model fills that role, which is why the walkthrough's UserRegisterValidator is the body type and the response source at once.

How do I get help learning this stack in Portuguese?

The Portuguese-speaking developer community around Python, Django, and TypeScript is active, and creators such as Gustavo Dev Doido publish courses through the Bootcamp do Dev Doido and a Crazystack Typescript learning path. See crazystack.com.br for the project's own material.

Turning the walkthrough into something you can ship

The point of this layout is not the folder names. It is that each layer has one reason to change: add a resource by adding a validator and a route module, add cross-cutting behavior by editing server.py, and let app.include_router() do the wiring. If that idea lives in a video you recorded, it can live in an article too. Skala Blog turns a YouTube URL into a transcription and then a written article, so the explanation you already gave stays readable long after the recording.

Skala Blog

Source video