Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

banner

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.

Next.js

SvelteKit

  • Architecture - SvelteKit application structure and patterns

Backend

Complete documentation about our server infrastructure, APIs, and services.

  • Overview - Introduction to Viventio's backend

Python

Getting Started

If you're new to the Viventio codebase, we recommend following this learning path:

  1. Start with the Overview - Read the README files in both backend and frontend sections.
  2. Understand the Architecture - Review the architecture documents to grasp system design.
  3. Learn the Guidelines - Familiarize yourself with our coding standards and conventions.
  4. 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

  1. Environment Setup
  2. Coding Standards & Conventions

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 useXXX prefix for hook file names (e.g., useGetUser.ts).
  • 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

  1. Structure Overview
  2. Folder Structure
  3. Applied Architectural Principles

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 Components

    Contains 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 Layer

    Each service follows the DAO + TanStack + Zod pattern:

    • __tests__/
      Unit tests for the service.

      • test.ts file 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 wrap dao.ts with 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.ts Defines 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.ts contains ONLY external data access logic.
  • It DOES NOT process state, UI or business logic.

TanStack Query Integration

  • queries.ts encapsulates hooks that uses dao.ts.
  • Controls caching logic, onSuccess and onError callback 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

  • vitest for testing.
  • @testing-library/react for testing with React Query.
  • msw for mocking HTTP requests.
  • jsdom for mocking the DOM.
  • @types/jsdom for typings for jsdom
  • @tanstack/react-query for 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

  1. Structure Overview
  2. Folder Structure
  3. Applied Architectural Principles

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 Components

    Contains 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 Layer

    Each service follows the DAO + TanStack + Zod pattern:

    • __tests__/
      Unit tests for the service.

      • test.ts file 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 wrap dao.ts with 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.ts Defines 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.ts contains ONLY external data access logic.
  • It DOES NOT process state, UI or business logic.

TanStack Query Integration

  • queries.ts encapsulates hooks that uses dao.ts.
  • Controls caching logic, onSuccess and onError callback 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

  1. Environment Setup
  2. Usage Instructions
  3. Coding Standards & Conventions

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

    See Docker Installation and Docker Compose Installation

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 typing module
  • Return Types: Always specify, use -> None for void functions
  • Mypy: Strict mode enabled

Layers

  1. Models (features/{feature}/model.py): Define database entities using SQLAlchemy 2.0+ with async support. Use Mapped for columns, relationship for associations, and enum.Enum for choice fields.

  2. Repositories (features/{feature}/repository.py): Handle all database interactions. Methods are async and return Result[T, E] from the pytresult library for error handling.

  3. Services (features/{feature}/service.py): Contain business logic, validation, and orchestration. Call repositories and perform checks before data operations.

  4. Routers (routers/v1/routes.py): Define FastAPI endpoints using dependency injection for services and authentication.

  5. 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_case for functions/variables, PascalCase for 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] from pytresult library
  • Success: return Ok(value)
  • Error: return Err(CustomError())
  • Custom Exceptions: Define in features/{feature}/exceptions.py and inherit from BaseError
  • 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 MappedAsDataclass and DeclarativeBase for type-safe models
  • Relationships: Define with relationship() and appropriate cascade options
  • Enums: Use str, enum.Enum for choice fields, mapped with SQLEnum
  • 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-cffi via features/shared/utils/password_hasher.py
  • Dependencies: get_current_logged_user

File Storage

  • S3 Integration: Use boto3 for image uploads in core/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-cov for 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.py with environment-based origins

Development Workflow

  1. Setup: poetry install && pre-commit install
  2. Development: task up for dev server
  3. Testing: task test for unit/integration tests
  4. Code Quality: task check-all before committing
  5. Database: task migrate-new for 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

  1. Project File Tree Structure
  2. Top-Level Structure
  3. Application Core (app/)
  4. Docs (docs/)
  5. Scripts (scripts/)
  6. Database Migrations (migrations/)
  7. Containerization (dockerfiles/)
  8. Testing (tests/)
  9. 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 the error_generator.py script.

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 the errors.json file.

  • migrate.py: Applies database migrations to the application. This script is used by the migrate.Dockerfile to 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

  1. 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.

  1. Configure Alembic with SQLAlchemy.

    2.1. Go to alembic/env.py and configure the SQLALCHEMY_DATABASE_URI variable with the database connection string using the method config.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 the target_metadata variable with the metadata object for the database using the method config.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.