Welcome to Viventio Technical Documentation
Welcome to Viventio's technical knowledge hub. This documentation has been created to help developers, architects, and technical team members understand, contribute to, and maintain our systems with excellence.
About This Repository
This documentation book contains all essential technical knowledge about the architecture, development patterns, testing, and best practices used at Viventio. Our goal is to ensure that the entire team has clear and organized access to the information needed to develop high-quality solutions.
Documentation Structure
Our documentation is organized into two main areas:
Frontend
Documentation about our user interfaces and client-side applications.
- Overview - Introduction to Viventio's frontend
- General Guidelines - Frontend code standards and conventions
Next.js
- Development Guidelines - Frontend code standards and conventions for Next.js
- Architecture - Next.js application structure and patterns
- Testing - Testing strategies for Next.js applications
SvelteKit
- Architecture - SvelteKit application structure and patterns
Backend
Complete documentation about our server infrastructure, APIs, and services.
- Overview - Introduction to Viventio's backend
Python
- Architecture - Backend systems structure and design
- Database - Schemas, models, and data best practices
- Development Guidelines - Code standards and conventions
- Testing - Backend testing strategies and practices
Getting Started
If you're new to the Viventio codebase, we recommend following this learning path:
- Start with the Overview - Read the README files in both backend and frontend sections.
- Understand the Architecture - Review the architecture documents to grasp system design.
- Learn the Guidelines - Familiarize yourself with our coding standards and conventions.
- Explore Testing Practices - Understand how we ensure code quality.
Technology Stack
Backend
Our backend infrastructure powers the core business logic and data management. We normally use Python for our backend development with a handcrafted architecture that heavily utilizes the following technologies:
- Poetry - Python package manager.
- FastAPI - Python framework for building APIs.
- SQLAlchemy - Object-relational mapping (ORM) for database interactions.
- PostgreSQL - Open-source relational database.
- Docker & Docker Compose - Containerization for consistent environments.
Frontend
We utilize modern frameworks to deliver exceptional user experiences:
- Next.js - React-based framework for production-grade applications
- SvelteKit - Modern framework for building fast, efficient web applications
How to Use This Documentation
- Browse by Topic - Use the structure above to navigate to specific areas.
- Search - Use your documentation tool's search feature to find specific topics.
- Contribute - Help keep this documentation up-to-date by submitting. improvements.
- Ask Questions - If something is unclear, reach out to the team.
Contributing
This documentation is a living resource that evolves with our systems. If you find gaps, errors, or opportunities for improvement, please contribute! Clear documentation benefits everyone on the team.
Support
For questions about this documentation or technical guidance, please reach out to Hícaro Dânrlley.
Maintained by: Hícaro Dânrlley - the CTO of Viventio.
Overview
General Guidelines
Next.js
Development Guidelines
This documentation is designed to establish a common idiom for writing and organizing code within our front-end Next.js project. It covers the environment setup, coding standards, and recommended learning resources and tooling.
Table of Contents
Environment Setup
Node and Package Manager
-
Using Node:
Install NodeJS globally if you haven't already by following these instructions here.
-
Using pnpm:
We standardize package management with pnpm. Install pnpm globally if you haven't already:
curl -fsSL https://get.pnpm.io/install.sh | sh -
Coding Standards & Conventions
Code Style Guidelines
-
Naming Conventions:
- Use camelCase for variables and functions.
- Use PascalCase for components and class names.
- Use UPPERCASE for constant files and configuration variables.
-
File and Directory Naming Coventions:
- For route folder names using lower-case with hyphens for routes
(
user-profile/page.tsx). - For hooks, use
useXXXprefix for hook file names (e.g.,useGetUser.ts).
- For route folder names using lower-case with hyphens for routes
(
-
Coding
- In order to avoid unexpected design breaks, don't edit Shadcn's components.
- In case you're using a different font family, always a generic fallback
font in your CSS, such
sans-serif.
Next.js Architecture
This document outlines the architecture of the project, including the components, their responsibilities, and how they interact with each other.
Table of Contents
Structure Overview
.
├── src
│ ├── lib
│ │ ├── features
│ │ │ ├── features-1
│ │ │ │ ├── components
│ │ │ │ │ ├── ...
│ │ │ │ └── services
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ │ ├── feature-2
│ │ │ │ ├── components
│ │ │ │ │ └── dashboard
│ │ │ │ │ ├── ...
│ │ │ │ └── services
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ │ ├── shared/
│ │ │ │ ├── components/
│ │ │ │ │ ├── app-navbar.tsx
│ │ │ │ │ └── _shadcn
│ │ │ │ │ ├── ...
│ │ │ │ └── services/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ ├── config/
│ │ │ ├── ...
│ ├── app/
│ │ ├── page.tsx
│ │ └── ...
Folder Structure
src/lib/
Contains all reusable logic, including components, services, utilities, and configurations. Organized as follows:
features/ – Feature-Specific Implementations
Implementations specific of UI components, services, hooks and more utilities to real application features (e.g. features/user, features/admin):
-
components/– Generic ComponentsContains UI components specific to the feature. This directory does not follow any specific pattern in order to allow for maximum flexibility to organize components as needed.
-
services/– Data Access LayerEach service follows the DAO + TanStack + Zod pattern:
-
__tests__/
Unit tests for the service.test.tsfile is the entry point for the tests.
-
daos.ts
Pure functions for direct data access (e.g., APIs, local databases).// lib/services/user/dao.ts import type { UserModel } from "./models"; export async function fetchUser(id: string): Promise<User> { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error("Failed to fetch user"); return res.json(); } export async function updateUser( id: string, data: UserModel, ): Promise<UserModel> { const res = await fetch(`/api/users/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("Failed to update user"); return res.json(); } -
queries.ts
TanStack Query hooks that wrapdao.tswith state and cache logic.// lib/services/user/queries.ts import { useQuery, useMutation, useQueryClient, } from "@tanstack/react-query"; import { fetchUser, updateUser } from "./dao"; import { userModelSchema } from "./models"; import type { User } from "./models"; // For building good query key factories, see: // https://tkdodo.eu/blog/effective-react-query-keys export const userQueryKeys = { all: ["user"] as const, detail: (id: string) => [...userQueryKeys.all, "detail", id] as const, }; export function useGetUserById(userId: string) { return useQuery({ queryKey: userQueryKeys.detail(userId), queryFn: () => fetchUser(userId), enabled: !!userId, // only fetch if userId is defined }); } export function useUpdateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (user: User) => { const parsed = userSchema.parse(user); return updateUser(parsed.id, parsed); }, onSuccess: (_data, user) => { queryClient.invalidateQueries({ queryKey: userQueryKeys.detail(user.id), }); }, }); } -
models.tsDefines data schemas and models using Zod for runtime validation and static type inference// lib/services/users/models.ts import { z } from "zod"; export const userModelSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), createdAt: z.string(), }); export type UserModel = z.infer<typeof userModelSchema>;
Usage Example:
// pages/user/[id].tsx import { useRouter } from "next/router"; import { useGetUserById, useUpdateUser } from "@/lib/services/user/queries"; import { useState, useEffect } from "react"; export default function UserPage() { const router = useRouter(); const { id } = router.query as { id: string }; const { data: user, isLoading } = useGetUserById(id); const { mutate: updateUser, isPending } = useUpdateUser(); const [form, setForm] = useState({ name: "", email: "" }); useEffect(() => { if (user) { setForm({ name: user.name, email: user.email }); } }, [user]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setForm({ ...form, [e.target.name]: e.target.value }); }; const handleSubmit = () => { if (user) { updateUser({ ...user, ...form }); } }; if (isLoading) return <p>Carregando...</p>; if (!user) return <p>Usuário não encontrado</p>; return ( <div> <h1>Editar Usuário</h1> <input name="name" value={form.name} onChange={handleChange} /> <input name="email" value={form.email} onChange={handleChange} /> <button onClick={handleSubmit} disabled={isPending}> Salvar </button> </div> ); }NOTE: Form handling is not properly implemented in this example. On a real-world application, you should use a form library React Hook Form.
-
features/shared/ – Shared implementations
Follows the same pattern as features/, but is intended for implementations
that are shared across multiple features.
utils/ – Generic Utilities
Pure helper functions such as:
- Formatting
- Validation
- Conversions
config/ – Global Configurations
Defines aspects like:
- Base API URL
- Environment variables
src/app/
Standard NextJS app/ folder.
Responsible for:
- Rendering actual pages.
- Composing views using modules from
src/lib/.
Applied Architectural Principles
Feature-Based Structure
Organize components and services per domain (e.g.: user/, admin/), not by
type.
DAO Pattern
dao.tscontains ONLY external data access logic.- It DOES NOT process state, UI or business logic.
TanStack Query Integration
queries.tsencapsulates hooks that usesdao.ts.- Controls caching logic,
onSuccessandonErrorcallback management.
Next.js Testing
This is a simple example of how to test services with Vitest, MSW, React Testing Library on a Next.js application.
Requirements
Packages
vitestfor testing.@testing-library/reactfor testing with React Query.mswfor mocking HTTP requests.jsdomfor mocking the DOM.@types/jsdomfor typings forjsdom@tanstack/react-queryfor state management.
How to write tests
Defining a query
We define a query in the queries.ts file located at the services directory
of the feature. For example, to define a query to get a user by ID, we can add
the following code:
// src/lib/features/feature1/services/queries.ts
import { useQuery } from "@tanstack/react-query";
import { fetchUser } from "./daos";
const userQueryKeys = {
all: () => ["users"],
byId: (id: string) => [...userQueryKeys.all(), id],
};
export const useGetUserByIdQuery = (id: string) => {
return useQuery({
queryKey: userQueryKeys.byId(id),
queryFn: async () => await fetchUser(id),
});
};
Mocking HTTP requests
We use Mock Service Worker to mock HTTP requests. This allows us to test the application without making real HTTP requests.
To mock a request, we need to define a route in the handlers.ts file located
at the __tests__ directory of the server, more specifically in the list of
handlers. For example, to mock a request to /users/:id, where :id can be
anything, we can add the following code:
// src/lib/features/feature1/services/__tests__/handlers.ts
import { BASE_API_URL } from "@/lib/config/consts";
import { http, HttpResponse } from "msw";
export const handlers = [
http.get(`${BASE_API_URL}/users/:id`, ({ params }) => {
const { id } = params;
if (id === "404") {
return HttpResponse.json({ error: "User not found" }, { status: 404 });
}
return HttpResponse.json({ id, name: "User 1" });
}),
];
Then, we can use the msw package to start the server and handle the requests.
import * as Feature1 from "@/lib/features/feature1/services/__tests__/handlers";
import { setupServer } from "msw/node";
export const server = setupServer(...Feature1.handlers);
For any other feature, add the handlers to the list of handlers. Let's say we have a second feature and we want to mock its handlers as well, we can add the following code:
import * as Feature1 from "@/lib/features/feature1/services/__tests__/handlers";
import * as Feature2 from "@/lib/features/feature2/services/__tests__/handlers";
import { setupServer } from "msw/node";
export const server = setupServer(...Feature1.handlers, ...Feature2.handlers);
Testing with React Query
We use the @tanstack/react-query package to manage state in our application.
We can use the renderHook function from the @testing-library/react package
to test the state of our application.
For example, to test the useGetUserByIdQuery hook, we can create a wrapper
function that sets up the QueryClient and provides it to the hook.
// src/lib/features/feature1/services/__tests__/queries.test.tsx
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useGetUserByIdQuery } from "../queries";
import { describe, test, expect } from "vitest";
import * as React from "react";
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false, // Disable retries for testing
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
describe("useGetUserByIdQuery", () => {
test("returns user when found", async () => {
const { result } = renderHook(() => useGetUserByIdQuery("1"), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.data).toBeDefined());
expect(result.current.isSuccess).toBeTruthy();
expect(result.current.data?.name).toBe("User 1");
});
test("handles user not found", async () => {
const { result } = renderHook(() => useGetUserByIdQuery("404"), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error?.message).toBe("User not found");
});
});
Testing with jsdom
We use the jsdom package to mock the DOM in our tests. This allows us to test
the application without creating a DOM environment.
To use jsdom, we need to install the @types/jsdom package and add the
following code to the vitest.config.ts file:
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { resolve } from "path";
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: [{ find: "@", replacement: resolve(__dirname, "./src") }],
},
});
And also setup the vitest.setup.ts file:
// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "@/lib/config/tests/server";
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
SvelteKit
SvelteKit Architecture
This document outlines the architecture of the project, including the components, their responsibilities, and how they interact with each other.
Table of Contents
Structure Overview
.
├── src
│ ├── lib
│ │ ├── features
│ │ │ ├── features-1
│ │ │ │ ├── components
│ │ │ │ │ ├── ...
│ │ │ │ └── services
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ │ ├── feature-2
│ │ │ │ ├── components
│ │ │ │ │ └── dashboard
│ │ │ │ │ ├── ...
│ │ │ │ └── services
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ │ ├── shared/
│ │ │ │ ├── components/
│ │ │ │ │ ├── app-navbar.svelte
│ │ │ │ │ └── _shadcn
│ │ │ │ │ ├── ...
│ │ │ │ └── services/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── test.ts
│ │ │ │ ├── daos.ts
│ │ │ │ ├── models.ts
│ │ │ │ └── queries.ts
│ │ ├── config/
│ │ │ ├── ...
│ ├── routes
│ │ ├── +page.svelte
│ │ └── ...
Folder Structure
src/lib/
Contains all reusable logic, including components, services, utilities, and configurations. Organized as follows:
features/ – Feature-Specific Implementations
Implementations specific of UI components, services, hooks and more utilities to real application features (e.g. features/user, features/admin):
-
components/– Generic ComponentsContains UI components specific to the feature. This directory does not follow any specific pattern in order to allow for maximum flexibility to organize components as needed.
-
services/– Data Access LayerEach service follows the DAO + TanStack + Zod pattern:
-
__tests__/
Unit tests for the service.test.tsfile is the entry point for the tests.
-
daos.ts
Pure functions for direct data access (e.g., APIs, local databases).// lib/services/user/daos.ts import type { User } from "./models"; export async function fetchUser(id: string): Promise<User> { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error("Failed to fetch user"); return res.json(); } export async function updateUser(id: string, data: User): Promise<User> { const res = await fetch(`/api/users/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error("Failed to update user"); return res.json(); } -
queries.ts
TanStack Query hooks that wrapdao.tswith state and cache logic.// lib/services/user/queries.ts import { queryOptions, createMutation } from "@tanstack/svelte-query"; import { fetchUser, updateUser } from "./dao"; import { userModelSchema } from "./models"; import type { UserModel } from "./models"; export const userQueryKeys = { all: ["user"] as const, detail: (id: string) => [...userQueryKeys.all, "detail", id] as const, }; export function useGetUserById(userId: string) { return queryOptions({ queryKey: userQueryKeys.detail(userId), queryFn: () => fetchUser(userId), }); } export function useUpdateUser() { return createMutation({ mutationFn: async (user: UserModel) => { const parsed = userModelSchema.parse(user); // Validate input return updateUser(parsed.id, parsed); }, }); } -
models.tsDefines data schemas and models using Zod for runtime validation and static type inference// lib/services/users/models.ts import { z } from "zod"; export const userModelSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), createdAt: z.string(), }); export type UserModel = z.infer<typeof UserModelSchema>;
-
features/shared/ – Shared implementations
Follows the same pattern as features/, but is intended for implementations
that are shared across multiple features.
utils/ – Generic Utilities
Pure helper functions such as:
- Formatting
- Validation
- Conversions
config/ – Global Configurations
Defines aspects like:
- Base API URL
- Environment variables
src/routes/
Standard SvelteKit folder.
Responsible for:
- Rendering actual pages.
- Composing views using modules from
lib/.
Applied Architectural Principles
Feature-Based Structure
Organize components and services per domain (e.g.: user/, admin/), not by
type.
DAO Pattern
dao.tscontains ONLY external data access logic.- It DOES NOT process state, UI or business logic.
TanStack Query Integration
queries.tsencapsulates hooks that usesdao.ts.- Controls caching logic,
onSuccessandonErrorcallback management.
Overview
Python
Development Guidelines
This documentation is designed to establish a common idiom for writing and organizing code within our back-end Python project. It covers the environment setup, folder organization, database management, error handling, testing and more.
Table of Contents
Environment Setup
Requirements
-
Unix-like operating system or Windows (preferrably with WSL)
-
Python 3.12 or higher
-
Poetry for managing dependencies
curl -sSL https://install.python-poetry.org | python3.12 - # -
Docker and Docker Compose
Installation
-
Activate virtual environment
eval $(poetry env activate) -
Install dependencies in the virtual environment
poetry install -
Install pre-commit hooks
pre-commit install
Usage Instructions
Run application locally
# this command will spin up the application with Docker Compose
task up
Test application locally
task test
To learn more about how testing works, check out the TESTING.md documentation.
Other useful commands
task --list
Coding Standards & Conventions
Python Version & Types
- Python: 3.12+ required
- Type Hints: Mandatory for all functions and methods using
typingmodule - Return Types: Always specify, use
-> Nonefor void functions - Mypy: Strict mode enabled
Layers
-
Models (
features/{feature}/model.py): Define database entities using SQLAlchemy 2.0+ with async support. UseMappedfor columns,relationshipfor associations, andenum.Enumfor choice fields. -
Repositories (
features/{feature}/repository.py): Handle all database interactions. Methods are async and returnResult[T, E]from thepytresultlibrary for error handling. -
Services (
features/{feature}/service.py): Contain business logic, validation, and orchestration. Call repositories and perform checks before data operations. -
Routers (
routers/v1/routes.py): Define FastAPI endpoints using dependency injection for services and authentication. -
Core Config (
core/config/): Centralized configuration for database, auth, storage, and server setup.
Formatting & Linting
- Formatter: Black
- Import Sorting: isort with Black profile
- Linter: flake8
- Naming:
snake_casefor functions/variables,PascalCasefor classes
Imports
# Standard library imports first
from typing import List, Optional
import asyncio
# Third-party imports
from result import Err, Ok, Result
from fastapi import Depends
from sqlalchemy import select
# Local imports (absolute paths)
from app.features.user.model import User
from app.core.config.db.postgres import Base
Error Handling
- Pattern: Use
Result[T, E]frompytresultlibrary - Success:
return Ok(value) - Error:
return Err(CustomError()) - Custom Exceptions: Define in
features/{feature}/exceptions.pyand inherit fromBaseError - Error Checking: Use
result.is_ok(),result.is_err(),result.unwrap(),result.err()
Database Patterns
- ORM: SQLAlchemy 2.0+ with async support (
AsyncSession,async_sessionmaker) - Models: Use
MappedAsDataclassandDeclarativeBasefor type-safe models - Relationships: Define with
relationship()and appropriate cascade options - Enums: Use
str, enum.Enumfor choice fields, mapped withSQLEnum - Migrations: Alembic for schema changes
Example Model:
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, init=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(SQLEnum(UserRole), default=UserRole.collector, nullable=False)
Repository Patterns
- Class Structure: Inherit from no base, inject
AsyncSession - Methods: All async, return
Result[T, E] - Queries: Use
select(),update(),delete()with proper filtering - Error Handling: Catch exceptions and return
Err(SomeDeveloperDefinedError(e))
Example Repository:
class UserRepository:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def get_by_id(self, id: int) -> Result[Optional[User], UserError]:
stmt = select(User).where(User.id == id)
query = await self.session.execute(stmt)
user = query.scalar()
return Ok(user)
Service Patterns
- Class Structure: Inject repository in
__init__ - Business Logic: Perform validation, checks, and call repository methods
- Validation: Check lengths, existence, and business rules before operations
- Error Propagation: Handle repository errors and return appropriate service errors
Example Service:
class UserService:
def __init__(self, repository: UserRepository) -> None:
self._repository = repository
async def create(self, name: str, cpf: str, password: str) -> Result[None, UserError]:
if len(name) < 4:
return Err(UserInvalidNameError("Name must be at least 4 characters"))
# ... more validation and repository call
Router Patterns
- Framework: FastAPI with Pydantic schemas
- Dependencies: Use
Depends()for services, auth, and pagination - Error Handling: Catch service errors and raise HTTP exceptions
- Schemas: Define request/response models in
routers/v1/schemas.py
Example Router:
@router.post("/users", response_model=UserResponse)
async def create_user(
request: UserCreateRequest,
service: UserService = Depends(get_user_service),
) -> UserResponse:
result = await service.create(request.name, request.cpf, request.password)
if result.is_err():
raise HTTPException(status_code=400, detail=str(result.err()))
return UserResponse.from_user(result.unwrap())
Authentication & Authorization
- JWT: Custom implementation in
core/config/auth/jwt.py - Password Hashing: Use
argon2-cffiviafeatures/shared/utils/password_hasher.py - Dependencies:
get_current_logged_user
File Storage
- S3 Integration: Use
boto3for image uploads incore/config/bs.py
Testing
- Framework: pytest with pytest-asyncio
- Structure:
tests/features/{domain}/v1/test_{component}.py - Async Tests: Use
@pytest.mark.asyncio - Fixtures: Define in
conftest.py - Coverage: Use
pytest-covfor reports
Security Best Practices
- Secrets: Never log or commit secrets; use environment variables
- Input Validation: Validate all inputs in services and schemas
- SQL Injection: Use parameterized queries via SQLAlchemy
- CORS: Configured in
server.pywith environment-based origins
Development Workflow
- Setup:
poetry install && pre-commit install - Development:
task upfor dev server - Testing:
task testfor unit/integration tests - Code Quality:
task check-allbefore committing - Database:
task migrate-newfor schema changes
Key Dependencies
- Web Framework: FastAPI
- ORM: SQLAlchemy[asyncio]
- Validation: Pydantic
- Error Handling: pytresult
- Password Hashing: argon2-cffi
- JWT: pyjwt
- Testing: pytest, pytest-asyncio, pytest-cov
- Linting: flake8, mypy, black, isort, bandit
Best Practices
- Async/Await: Use for all I/O operations (DB, HTTP, file operations)
- Dependency Injection: Prefer over global state
- Single Responsibility: Each class/method has one purpose
- Immutable Data: Avoid mutating inputs
- Error Messages: Provide clear, user-friendly error messages
- Documentation: Use docstrings for public methods (Google style)
- Comments: Avoid in-code comments unless necessary for complex logic
- Version Control: Follow conventional commits
Architecture
This document outlines the architecture of the project, including the components, their responsibilities, and how they interact with each other.
Table of Contents
- Project File Tree Structure
- Top-Level Structure
- Application Core (
app/) - Docs (
docs/) - Scripts (
scripts/) - Database Migrations (
migrations/) - Containerization (
dockerfiles/) - Testing (
tests/) - Root-Level Configuration
Project File Tree Structure
Here's the detailed file tree structure of your project:
├── README.md
├── docs/
│ ├── errors.md
│ └── ...
├── scripts/
│ ├── error_generator.py
│ └── migrate.py
├── migrations/
│ ├── README
│ ├── __init__.py
│ ├── env.py
│ ├── script.py.mako
│ └── versions
│ └── ...
├── alembic.ini
├── app/
│ ├── __init__.py
│ ├── __main__.py
│ ├── core/
│ │ └── config/
│ │ └── ...
│ ├── features
│ │ ├── __init__.py
│ │ ├── feature-1/
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ ├── model.py
│ │ │ ├── repository.py
│ │ │ └── service.py
│ │ ├── feature-2/
│ │ │ ├── __init__.py
│ │ │ ├── exceptions.py
│ │ │ ├── model.py
│ │ │ ├── repository.py
│ │ │ └── service.py
│ │ └── feature-3/
│ │ ├── __init__.py
│ │ ├── exceptions.py
│ │ ├── model.py
│ │ ├── repository.py
│ │ └── service.py
│ ├── main.py
│ └── routers/
│ ├── __init__.py
│ └── vX
│ ├── __init__.py
│ ├── feature_1/
│ │ ├── dependencies.py
│ │ ├── routes.py
│ │ └── schemas.py
│ ├── feature_2
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ ├── routes.py
│ │ └── schemas.py
│ ├── feature_3
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ ├── routes.py
│ │ └── schemas.py
│ └── ...
├── dev.compose.yml
├── dockerfiles
│ ├── migrate.Dockerfile
│ └── server.Dockerfile
├── env.py
├── isort.cfg
├── mypy.ini
├── poetry.lock
├── prod.compose.yml
├── pyproject.toml
├── pytest.ini
├── errors.json
└── tests
├── __init__.py
├── conftest.py
└── features
├── __init__.py
└── feature_1
├── vX
│ ├── __init__.py
│ ├── test_feature_1_integration.py
│ ├── test_feature_1_service.py
│ └── test_feature_1_repository.py
└── ...
Top-Level Structure
The project is organized into several key directories:
-
app/: Contains the core application logic. -
migrations/: Handles database migrations. -
dockerfiles/: Stores Docker configurations for various services. -
tests/: Holds all application tests. -
Root-level configuration files: Various configuration and project management files (
pyproject.toml,poetry.lock,pytest.ini,alembic.ini,dev.compose.yml,prod.compose.yml,env.py,isort.cfg,mypy.ini,README.md).
Application Core (app/)
This is the heart of the application, encapsulating all business logic, data models, and API endpoints.
Core Configuration (app/core/config/)
This directory centralizes application-wide configurations, ensuring that environment-specific settings, database connections, and caching mechanisms are managed consistently.
config/: This directory generally holds configuration files for various aspects of the application, such as database settings, cache configurations, auth configurations, server parameters, and many more. The...indicates that specific configuration files (like redis.py, postgres.py, server.py) or directories would reside within this directory.
Features (app/features/)
This is where the domain logic for different functional areas (features) of the application resides. Each subdirectory represents a distinct feature with its own models, business logic, and data access. This promotes modularity, making it easier to develop, test, and maintain individual components.
feature_1/,feature_2/,feature_3/: These represent distinct functional areas or modules within the application (e.g., users, products, orders).
Within each feature directory (e.g., feature_1), you'll typically find:
-
model.py: Defines the SQLAlchemy ORM models, representing the data structures and relationships within the database. -
repository.py: Contains the data access logic, abstracting interactions with the database (e.g., CRUD operations for models). This layer decouples the business logic from the specifics of the database. -
service.py: Implements the core business logic for the feature. It orchestrates interactions between repositories, applies business rules, and performs operations related to the feature. -
exceptions.py: Defines custom exceptions specific to the feature, improving error handling and clarity.
The following diagram illustrates the interaction between the Routes, Services, and Repositories, along with their engagement with external services like databases and other APIs.
graph TD
subgraph External Services
DB[Database]
API[External API]
end
subgraph Application
Routes[API Routes]
Services[Business Services]
Repos[Data Repositories]
end
Routes --> Services
Services --> Repos
Repos --> DB
Repos --> API
Main Application Entry (app/main.py)
This file typically serves as the main entry point for the FastAPI application, where routers are included, and the application instance is created.
API Routers (app/routers/vX/)
This directory structures the API endpoints for different versions of your API. vX indicates the version X. Each subdirectory within vX corresponds to a feature and defines its specific API routes.
Within each feature's router directory (e.g., feature-1):
-
dependencies.py: Defines FastAPI dependency injection functions. These are reusable components for authentication, authorization, database sessions, or other prerequisites for routes. -
routes.py: Contains the actual FastAPI route definitions (API endpoints) for the feature. It handles HTTP requests and calls the appropriate service layer methods. -
schemas.py: Defines Pydantic models for request and response data validation, ensuring data integrity for API interactions.
Docs (docs/)
This directory contains documentation files for the project, including error codes, architecture diagrams, and other project-related information.
errors.md: Contains a list of custom exceptions and their corresponding error codes. This document is generated automatically by using theerror_generator.pyscript.
Scripts (scripts/)
This directory contains scripts that automate various tasks, such as generating error messages, running database migrations, or deploying the application.
-
error_generator.py: Generates Python code with custom exceptions for errors specified in theerrors.jsonfile. -
migrate.py: Applies database migrations to the application. This script is used by themigrate.Dockerfileto apply migrations on startup. It is not really meant to be used directly.
Database Migrations (migrations/)
Alembic is used for database schema migrations. See Alembic for more information.
Containerization (dockerfiles/)
This directory holds Dockerfiles, which define how your application's services are built into Docker images for consistent deployment across different environments.
-
server.Dockerfile: Defines the Docker image for your main application server. -
migrate.Dockerfile: Defines the Docker image for applying database migrations.
Testing (tests/)
This directory contains all automated tests for the application, ensuring code quality and functionality.
-
conftest.py: Contains pytest fixtures and hooks that can be shared across multiple test files (e.g., setting up a test database, mock clients). -
vX/: Contains all test files for individual features or API endpoints within version X of your API.-
feature_1/test_feature_1_integration.py: Test the interaction between the routes, services, and repositories for a specific feature. -
feature_1/test_feature_1_service.py: Test the business logic for a specific feature. -
feature_1/test_feature_1_repository.py: Test the data access logic for a specific feature.
-
Root-Level Configuration
These files manage project dependencies, code style, development environments, and other project-wide settings.
alembic.ini: Configuration file for Alembic.dev.compose.yml: Docker Compose configuration for local development environments.env.py: Handles environment variable loading.isort.cfg: Configuration for isort, a tool to sort Python imports.mypy.ini: Configuration for mypy, a static type checker for Python.poetry.lock: Locks the exact versions of project dependencies.prod.compose.yml: Docker Compose configuration for production environments.pyproject.toml: Defines project metadata and dependencies (used by Poetry).pytest.ini: Configuration file for pytest, the testing framework.errors.json: List of custom exceptions and their corresponding error codes.README.md: Project documentation.
Database
SQLAlchemy and Alembic
This documentation explains how to set up SQLAlchemy and Alembic for a new project.
This document is more intended for developers who are setting up a new project from scratch.
Requirements
- Python 3.12 or greater.
- Poetry (see installation instructions)
- SQLAlchemy (
poetry add sqlalchemy) for database connection and ORM. - Alembic (
poetry add alembic) for database migrations.
Setup
- Initialize Alembic environment.
alembic init alembic
This will create a alembic.ini file in the project root directory and a
directory called alembic that contains the Alembic version control scripts.
-
Configure Alembic with SQLAlchemy.
2.1. Go to
alembic/env.pyand configure theSQLALCHEMY_DATABASE_URIvariable with the database connection string using the methodconfig.set_main_option.For example:
# RECOMMENDED: use environment variables for setting the database URL # because it might contain sensitive informations config.set_main_option("sqlalchemy.url", "sqlite:///app.db")2.2. Still in
alembic/env.py, configure thetarget_metadatavariable with the metadata object for the database using the methodconfig.set_main_option.For example:
# Base class is used for all models in the application. It contains the # metadata for the database. target_metadata = Base.metadata
Testing
This document explains how to test the application.
Technologies
How to test a feature
In the current architecture, each feature is divided into three parts: a route, a service, and a repository.
A route is the endpoint of the feature defined using FastAPI. A service is a class that implements the business logic of the feature. A repository is a class that interacts with the database or any other external service, such as cache, message queue, or a third-party API.
In order to test all three parts of a feature, we need to create integrations tests that test the interaction between the route, service, and repository. Also we need to create tests for the service and repository separately.
Example
Let's test a feature called users that has a route, a service, and a
repository.
# app/routers/v1/routes.py
class DeleteUserResponse(pydantic.BaseModel):
message: str
@ROUTER.delete("/users/me", response_model=DeleteUserResponse)
async def delete_current_user(
current_user: Annotated[User, Depends(get_current_logged_user)],
user_service: Annotated[UserService, Depends(get_user_service)],
) -> DeleteUserResponse:
result = await user_service.delete(current_user.id)
if result.is_err():
error = result.err()
match error:
case UserNotFoundError():
msg = f"User with id {current_user.id} not found"
raise UserNotFoundException(msg)
case _:
raise InternalException(str(error))
return DeleteUserResponse(message="User deleted successfully")
# app/features/users/service.py
class UserService:
async def get_by_id(self, id: int) -> Result[User, UserError]:
res = await self._repository.get_by_id(id)
if res.is_err():
err = res.err()
assert err is not None
return Err(err)
user = res.unwrap()
if user is None:
return Err(UserNotFoundError())
return Ok(user)
async def delete(self, id: int) -> Result[None, UserError]:
user_result = await self.get_by_id(id)
if user_result.is_err():
err = user_result.err()
assert err is not None
return Err(err)
user = user_result.unwrap()
return await self._repository.delete(user.id)
# app/features/users/repository.py
class UserRepository:
async def delete(self, user_id: int) -> Result[None, UserError]:
try:
stmt = delete(User).where(User.id == user_id)
await self.session.execute(stmt)
await self.session.commit()
return Ok(None)
except Exception as e:
await self.session.rollback()
return Err(UserError(str(e)))
Now we can test everything:
# tests/features/users/test_users_integration.py
@pytest.mark.asyncio
async def test_delete_current_user_happy_path(
api_client: httpx.AsyncClient,
user_service: UserService,
mock_user: User,
) -> None:
login = await api_client.post(
"/api/v1/sign-in",
json={"cpf": mock_user.cpf, "password": DEFAULT_PASSWORD},
)
assert login.status_code == 200
token = login.json()["token"]
user = (await user_service.get_by_cpf("12345678901")).unwrap()
user_id = user.id
delete_response = await api_client.delete(
"/api/v1/users/me",
headers={"Authorization": f"Bearer {token}"},
)
assert delete_response.status_code == 200
assert (await user_service.get_by_id(user_id)).err_is(UserNotFoundError)
# tests/features/users/test_users_service.py
@pytest.mark.asyncio
async def test_delete_user_happy_path(
user_service: UserService,
mock_user: User,
) -> None:
delete_result = await user_service.delete(mock_user.id)
assert delete_result.is_ok()
get_result = await user_service.get_by_id(mock_user.id)
assert get_result.err_is(UserNotFoundError)
# tests/features/users/test_users_repository.py
@pytest.mark.asyncio
async def test_delete_user_happy_path(
session: AsyncSession,
user_repository: UserRepository,
mock_user: User,
) -> None:
user_id = mock_user.id
result = await user_repository.delete(user_id)
assert result.is_ok()
deleted_user = await session.get(User, user_id)
assert deleted_user is None
You see those parameters in the tests: api_client, user_service,
mock_user, session, user_repository.
They are all fixtures that are defined in conftest.py. The conftest.py file
is the entry point for all tests and it looks like something like this:
# tests/conftest.py
# ==== SERVICES ====
@pytest_asyncio.fixture
async def user_service(user_repository: UserRepository) -> UserService:
return UserService(user_repository)
@pytest_asyncio.fixture
async def real_estate_service(
real_estate_repository: RealEstateRepository,
) -> RealEstateService:
return RealEstateService(real_estate_repository)
# ==== REPOSITORIES ====
@pytest_asyncio.fixture
async def user_repository(session: AsyncSession) -> UserRepository:
return UserRepository(session)
@pytest_asyncio.fixture
async def real_estate_repository(
session: AsyncSession,
) -> RealEstateRepository:
return RealEstateRepository(session)
# ==== API ====
@pytest_asyncio.fixture()
async def api_client(session: AsyncSession) -> AsyncGenerator[
httpx.AsyncClient,
None,
]:
def get_session_override() -> AsyncSession:
return session
@asynccontextmanager
async def _override_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
yield
APP.router.lifespan_context = _override_lifespan
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=APP),
base_url="http://test",
) as client:
APP.dependency_overrides[get_session] = get_session_override
yield client
APP.dependency_overrides.clear()
# ==== DATABASE ====
@pytest.fixture(scope="session")
def postgres_container() -> Generator[PostgresContainer, None, None]:
with PostgresContainer("postgres:17", driver="asyncpg") as container:
yield container
@pytest_asyncio.fixture
async def engine(postgres_container: PostgresContainer) -> AsyncEngine:
return get_async_engine(postgres_container.get_connection_url())
@pytest_asyncio.fixture
async def sessionmaker(engine: AsyncEngine) -> AsyncSessionMaker:
return get_async_sessionmaker(engine)
@pytest_asyncio.fixture
async def session(
sessionmaker: AsyncSessionMaker,
) -> AsyncGenerator[AsyncSession, None]:
async with sessionmaker() as session:
async with session.bind.begin() as conn:
assert isinstance(conn, AsyncConnection)
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
yield session
# ==== OTHERS ====
@pytest_asyncio.fixture
async def signed_user_token(
api_client: httpx.AsyncClient,
mock_user: User,
user_service: UserService,
) -> str:
cpf = "12345678901"
name = "Test User"
res = await user_service.create(cpf, name, DEFAULT_PASSWORD)
assert res.is_ok()
response = await api_client.post(
"api/v1/sign-in",
json={
"cpf": mock_user.cpf,
"password": DEFAULT_PASSWORD,
},
)
assert response.status_code == 200
output = response.json()
assert "token" in output
token: str = output["token"]
return token
@pytest_asyncio.fixture
async def mock_user(user_service: UserService) -> User:
cpf = "12345678901"
name = "Test User"
res = await user_service.create(name, cpf, DEFAULT_PASSWORD)
assert res.is_ok()
res = await user_service.get_by_cpf(cpf)
assert res.is_ok()
res = res.unwrap()
assert res is not None
user: User = res
return user
Fixtures are really useful to avoid code duplication and to make our tests easier to read and maintain.