Backend Detail

1. OverviewCopied!

This document provides a detailed explanation of the backend system for the Full-Stack Todo List application. The backend is a RESTful API service written in Go, responsible for:

  • Handling user authentication (Email/Password via JWT, Google OAuth).

  • Managing CRUD operations for Todos, Tags, and Subtasks.

  • Handling image attachment uploads and deletions via Google Cloud Storage (GCS).

  • Persisting data in a PostgreSQL database.

  • Providing data to the Next.js frontend application.

2. Architecture: Hexagonal (Ports & Adapters)Copied!

The backend employs the Hexagonal Architecture, also known as Ports and Adapters. The core principle is to isolate the application's central business logic (the "hexagon core") from external concerns like the database, web framework, or third-party APIs.

  • Core (Inside the Hexagon): Contains the essential business rules and entities (internal/domain, internal/service). It knows nothing about how data is stored or how users interact with it.

  • Ports (On the Hexagon boundary): These are interfaces defined by the core or application layer.

    • Driving Ports: Define how external actors drive the application (e.g., TodoService interface defines how to create a todo). Implemented by the application layer (internal/service).

    • Driven Ports: Define how the application is driven by external systems (e.g., TodoRepository interface defines how to save a todo to persistence). Defined where needed, often alongside the services or repositories.

  • Adapters (Outside the Hexagon): Concrete implementations that connect the ports to specific technologies.

    • Driving Adapters: Adapt external input to calls on the driving ports (e.g., HTTP handlers in internal/api call methods on TodoService).

    • Driven Adapters: Implement the driven port interfaces to interact with specific infrastructure (e.g., pgxTodoRepository in internal/repository implements TodoRepository using PostgreSQL; gcsStorageService in internal/service implements FileStorageService).

Benefits:

  • Testability: The core logic can be tested independently by mocking the port interfaces.

  • Maintainability: Infrastructure changes (e.g., changing the database, web framework, or cloud storage provider) only require changing or adding adapters, leaving the core untouched.

  • Technology Agnosticism: The core doesn't depend on specific frameworks or databases.

3. Directory Structure & Mapping to Hexagonal ArchitectureCopied!

backend/
├── cmd/server/main.go       # Application entry point, wiring, server setup (Infrastructure)
├── internal/
│   ├── api/                 # Driving Adapter (HTTP REST API Layer)
│   │   ├── handlers.go      # HTTP request handlers implementing generated ServerInterface
│   │   ├── middleware.go    # HTTP middleware (e.g., Auth)
│   │   └── openapi_*.go     # Code generated by oapi-codegen (types, server stubs)
│   ├── auth/                # Adapter/Infrastructure (Auth mechanism details)
│   │   ├── jwt.go           # JWT claims struct
│   │   ├── oauth.go         # Google OAuth provider implementation
│   │   └── state.go         # OAuth state signing/verification
│   ├── cache/               # Driven Port & Adapter (Caching)
│   │   └── cache.go         # Cache interface (Port) & go-cache implementation (Adapter)
│   ├── config/              # Infrastructure (Configuration Loading)
│   │   └── config.go        # Viper configuration loading and structs
│   ├── domain/              # Core Hexagon (Business Entities & Rules)
│   │   ├── errors.go        # Domain-specific errors
│   │   ├── todo.go          # Todo entity, TodoStatus enum
│   │   ├── tag.go           # Tag entity
│   │   ├── user.go          # User entity
│   │   └── subtask.go       # Subtask entity
│   ├── repository/          # Driven Ports & Adapters (Database Persistence)
│   │   ├── interfaces.go    # Repository interfaces (Driven Ports), Registry
│   │   ├── db.go            # Database connection pool setup
│   │   ├── *_repo.go        # Pgx/sqlc implementations (Driven Adapters)
│   │   ├── *_repo_cache.go  # Caching decorators (Driven Adapters wrapping DB Adapters)
│   │   └── sqlc/            # SQLC specific files
│   │       ├── queries/     # Raw *.sql queries
│   │       ├── generated/   # *.sql.go files generated by sqlc
│   │       └── ...
│   └── service/             # Application Layer (Use Cases, Orchestration)
│       ├── interfaces.go    # Service interfaces (Driving Ports), FileStorageService (Driven Port)
│       ├── *_service.go     # Service implementations, business logic orchestration
│       ├── validation.go    # Input validation logic
│       └── gcs_storage.go   # GCS implementation of FileStorageService (Driven Adapter)
├── migrations/              # Database schema migrations (Infrastructure)
│   ├── *.up.sql
│   └── *.down.sql
├── bin/                     # Compiled binary output (Ignored by Git)
├── openapi.yaml             # API Specification (Source of Truth for API)
├── sqlc.yaml                # SQLC configuration file
├── go.mod                   # Go module definition
├── go.sum                   # Go module checksums
├── Makefile                 # Build, run, generate, migrate tasks
├── .air.toml                # Config for 'air' live reload (dev)
├── config.yaml              # Local configuration (Ignored by Git)
└── gcs-credentials.json     # GCS credentials (Ignored by Git)

4. Detailed Code BreakdownCopied!

cmd/server/main.go

  • Purpose: The main entry point of the application. It's responsible for initializing all components and starting the HTTP server.

  • Key Steps:

    1. Load Configuration: Reads settings from config.yaml and environment variables using Viper (config.LoadConfig).

    2. Setup Logger: Configures the slog logger based on the config.

    3. Database Connection: Creates a pgxpool connection pool (repository.NewConnectionPool).

    4. Run Migrations: Applies pending database migrations using golang-migrate.

    5. Initialize Cache: Creates the in-memory cache instance (cache.NewMemoryCache).

    6. Initialize Repositories: Creates the RepositoryRegistry, injecting the DB pool, cache, and logger. This wires up the base repositories and their caching decorators (repository.NewRepositoryRegistry).

    7. Initialize Services: Creates instances of each service (AuthService, TodoService, etc.), injecting their dependencies (repositories, other services, config).

    8. Initialize API Handler: Creates the ApiHandler, injecting the services, config, and logger.

    9. Setup Router (Chi): Creates a Chi router, applies middleware (logging, CORS, timeout, request ID, recovery, custom structured logging, authentication).

    10. Define Routes: Mounts the oapi-codegen generated handlers onto the router, applying the authentication middleware to protected routes. Defines public routes (login, signup, OAuth) and health checks separately.

    11. Start Server: Configures and starts the http.Server.

    12. Graceful Shutdown: Listens for OS signals (SIGINT, SIGTERM) to shut down the server cleanly.

internal/domain

  • Purpose: Represents the core business concepts and rules, independent of any technology.

  • Contents:

    • Go structs (User, Todo, Tag, Subtask) mirroring the database schema but using Go types (e.g., *string for nullable text, uuid.UUID). JSON tags are for potential direct use, but API models are preferred for responses.

    • Enums/Constants (e.g., TodoStatus).

    • Custom domain error variables (ErrNotFound, ErrConflict, etc.) used throughout the application for consistent error handling.

  • Key Principle: This layer should not import packages from internal/api, internal/repository, internal/config, or specific framework/database libraries.

internal/service

  • Purpose: Contains the application's use case logic. Services orchestrate operations, coordinate repositories, and enforce business rules that span multiple entities.

  • interfaces.go: Defines the interfaces for each service (e.g., TodoService). These act as the Driving Ports that the API layer calls. It also defines interfaces for Driven Ports needed by services, like FileStorageService.

  • *_service.go files: Implement the service interfaces.

    • Example (todo_service.go): CreateTodo validates input, potentially validates tag ownership via TagService, calls TodoRepository.Create, calls TodoRepository.SetTags, and returns the result. DeleteTodo calls TodoRepository.Delete and then potentially calls FileStorageService.Delete.

    • Services depend on repository interfaces, not concrete implementations.

  • validation.go: Provides reusable validation functions for input data (e.g., username length, email format, hex color). Services call these validators.

  • gcs_storage.go: The concrete implementation (Driven Adapter) for the FileStorageService interface, using the Google Cloud Storage client library.

internal/repository

  • Purpose: Abstract the data persistence mechanism.

  • interfaces.go: Defines the repository interfaces (UserRepository, TodoRepository, etc.) - these are the Driven Ports the service layer depends on. It also includes the RepositoryRegistry which simplifies dependency injection by bundling all repository implementations.

  • db.go: Helper function to establish the pgxpool connection pool.

  • *_repo.go files: Concrete implementations (Driven Adapters) of the repository interfaces using PostgreSQL.

    • They rely heavily on the sqlc-generated code (internal/repository/sqlc/generated).

    • They contain mapping functions (mapDbTodoToDomain, etc.) to convert between the sqlc-generated structs and the internal/domain structs.

    • They handle database-specific error mapping (e.g., converting pgx.ErrNoRows to domain.ErrNotFound, checking for unique constraint violations (23505) to return domain.ErrConflict).

  • *_repo_cache.go files: Caching decorators (Driven Adapters).

    • These also implement the repository interfaces (e.g., TagRepository).

    • They hold a reference to the next repository (the actual DB implementation) and the cache.Cache instance.

    • For read operations (GetByID), they check the cache first. On a miss, they call next.GetByID, store the result in the cache, and then return it.

    • For write operations (Create, Update, Delete), they call next.* first, and if successful, they invalidate (delete) the corresponding entry from the cache.

  • internal/repository/sqlc/: Contains the SQL files and generated code.

    • queries/*.sql: Raw SQL queries with sqlc annotations (-- name: CreateUser :one). This is where you define your database interactions.

    • generated/*.go: Go code automatically generated by sqlc based on the queries and schema. Do not edit these files manually. They provide type-safe functions to execute your SQL.

internal/api

  • Purpose: The Driving Adapter that handles HTTP requests and responses. It adapts the web world to the application's service layer.

  • openapi_*.go files: Generated by oapi-codegen from openapi.yaml. Includes:

    • openapi_types.go: Go structs representing the API request/response models defined in the OpenAPI spec.

    • openapi_generated.go: Defines the ServerInterface (which handlers must implement) and the Chi router setup function (HandlerWithOptions) that connects URL paths to handler methods.

  • handlers.go: Implements the ServerInterface.

    • Each handler function typically:

      1. Extracts the userID from the context (set by middleware).

      2. Parses and validates path parameters or the request body (using parseAndValidateBody helper and potentially mapping API models to service input DTOs).

      3. Calls the appropriate method on a service from the ServiceRegistry.

      4. Handles errors returned by the service, mapping them to appropriate HTTP status codes using SendJSONError.

      5. Maps the domain object(s) returned by the service to the API response model(s) (e.g., mapDomainTodoToApi).

      6. Sends the successful JSON response using SendJSONResponse.

  • middleware.go: Contains custom HTTP middleware.

    • AuthMiddleware: Checks for JWT in header/cookie, validates it using AuthService, and injects the userID into the request context. Public paths are skipped.

    • Helper functions like GetUserIDFromContext.

internal/auth

  • Purpose: Encapsulates the logic specific to authentication mechanisms. Acts as Adapters for auth protocols.

  • jwt.go: Defines the custom Claims struct used for JWTs.

  • oauth.go: Implements the OAuthProvider interface specifically for Google. Handles interactions with Google's OAuth endpoints (getting auth URL, exchanging code, fetching user info).

  • state.go: Provides functions to sign and verify the state parameter used in the OAuth flow to prevent CSRF attacks.

internal/cache

  • Purpose: Provides caching capabilities.

  • cache.go: Defines the Cache interface (Driven Port) and the memoryCache implementation (Driven Adapter) using patrickmn/go-cache.

internal/config

  • Purpose: Handles loading and accessing application configuration.

  • config.go: Defines structs mirroring the config.yaml structure and uses Viper to load configuration from the file and environment variables, applying defaults.

migrations/

  • Purpose: Stores SQL scripts for database schema changes.

  • Managed by golang-migrate. Each change has an up.sql (apply change) and down.sql (revert change) script, prefixed with a sequential version number.

Root Files (openapi.yaml, sqlc.yaml, Makefile)

  • openapi.yaml: The authoritative definition of the REST API. Used by oapi-codegen to generate code and can be used to generate API documentation or client SDKs.

  • sqlc.yaml: Configures sqlc regarding database engine, query/schema paths, generated package name, and type overrides.

  • Makefile: Provides convenient commands (make run, make generate, make migrate-up, etc.) to automate common development tasks.

5. Architectural & Technology DecisionsCopied!

  • Go: Chosen for its performance, simplicity, strong concurrency primitives, static typing, and excellent standard library, making it well-suited for building robust and efficient backend APIs.

  • Hexagonal Architecture: Selected to create a loosely coupled, highly testable, and maintainable system where the core business logic is independent of infrastructure details. This allows easier adaptation to future changes (e.g., different database, adding gRPC).

  • Chi Router: A lightweight, idiomatic Go router known for its performance and compatibility with standard net/http. It provides good middleware support.

  • OpenAPI & oapi-codegen: A spec-first approach ensures a well-defined API contract. Code generation reduces boilerplate for server stubs and request/response handling, improving consistency between the spec and implementation.

  • PostgreSQL: A powerful, reliable, and feature-rich open-source relational database.

  • pgx Driver: Preferred native Go driver for PostgreSQL, known for better performance and features compared to database/sql wrappers over lib/pq.

  • sqlc: Strikes a balance between raw SQL control and type safety. It avoids the complexities and potential "magic" of full ORMs while preventing typos and providing compile-time checks for SQL interactions.

  • golang-migrate: A standard and widely used tool for managing database schema migrations in Go projects.

  • GCS: Using a managed cloud object storage service is more scalable, reliable, and cost-effective for storing file attachments (like images) than storing them in the database or local filesystem, especially in distributed deployments.

  • JWT for Session Management: Standard stateless authentication mechanism suitable for APIs. Complemented by secure, HTTP-only cookies for browser-based clients.

  • Google OAuth: Provides a convenient and secure way for users to sign in using their existing Google accounts.

  • In-Memory Cache (go-cache): A simple way to introduce caching for performance improvement in single-instance deployments or development. Easy to swap later for a distributed cache like Redis if horizontal scaling is needed, thanks to the Cache interface and decorator pattern.

  • slog: The standard Go library for structured logging, improving log readability and enabling easier parsing/analysis.

  • Viper: Robust library for handling configuration from multiple sources (files, env vars, defaults).

6. Key Request Flows (Examples)Copied!

User Login (Email/Password)

  1. Frontend: Sends POST request to /api/v1/auth/login with email/password JSON body.

  2. Backend (main.go / Chi): Routes the request to ApiHandler.LoginUserApi.

  3. handlers.go (LoginUserApi): Parses the JSON body into models.LoginRequest. Calls AuthService.Login.

  4. auth_service.go (Login): Validates input. Calls UserRepository.GetByEmail. Compares the hashed password using bcrypt. If valid, calls AuthService.GenerateJWT.

  5. auth_service.go (GenerateJWT): Creates JWT claims (including UserID, expiry) and signs the token using the configured secret.

  6. handlers.go (LoginUserApi): Receives the token from the service. Sets the JWT as an HTTP-only cookie in the response headers. Sends a 200 OK response with the token in the JSON body (for non-browser clients).

  7. Frontend: Receives the response. Stores the token (if needed) or relies on the cookie for subsequent requests. Updates auth state (via Zustand).

Create Todo

  1. Frontend: Sends POST request to /api/v1/todos with Todo details (title, description, tags, etc.) in the JSON body and the JWT (via cookie or Authorization header).

  2. Backend (main.go / Chi): Routes the request. AuthMiddleware validates the JWT and injects the userID into the request context. The router calls ApiHandler.CreateTodo.

  3. handlers.go (CreateTodo): Extracts userID from context. Parses JSON body into models.CreateTodoRequest. Maps API model to service.CreateTodoInput. Calls TodoService.CreateTodo.

  4. todo_service.go (CreateTodo): Validates input. If TagIDs are present, calls TagService.ValidateUserTags. Calls TodoRepository.Create to insert the main todo data. If tags were provided, calls TodoRepository.SetTags (which likely runs in a transaction to clear old and add new tag associations).

  5. repository/todo_repo.go (Create, SetTags): Uses sqlc-generated functions to execute the corresponding SQL INSERT and DELETE/INSERT operations against the PostgreSQL database.

  6. todo_service.go (CreateTodo): Returns the created domain.Todo object.

  7. handlers.go (CreateTodo): Receives the domain Todo. Maps it to models.Todo. Sends a 201 Created JSON response.

  8. Frontend: Receives the response. Updates UI state (e.g., via React Query invalidation/update).