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.,
TodoServiceinterface 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.,
TodoRepositoryinterface 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/apicall methods onTodoService). -
Driven Adapters: Implement the driven port interfaces to interact with specific infrastructure (e.g.,
pgxTodoRepositoryininternal/repositoryimplementsTodoRepositoryusing PostgreSQL;gcsStorageServiceininternal/serviceimplementsFileStorageService).
-
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:
-
Load Configuration: Reads settings from
config.yamland environment variables using Viper (config.LoadConfig). -
Setup Logger: Configures the
sloglogger based on the config. -
Database Connection: Creates a
pgxpoolconnection pool (repository.NewConnectionPool). -
Run Migrations: Applies pending database migrations using
golang-migrate. -
Initialize Cache: Creates the in-memory cache instance (
cache.NewMemoryCache). -
Initialize Repositories: Creates the
RepositoryRegistry, injecting the DB pool, cache, and logger. This wires up the base repositories and their caching decorators (repository.NewRepositoryRegistry). -
Initialize Services: Creates instances of each service (
AuthService,TodoService, etc.), injecting their dependencies (repositories, other services, config). -
Initialize API Handler: Creates the
ApiHandler, injecting the services, config, and logger. -
Setup Router (Chi): Creates a Chi router, applies middleware (logging, CORS, timeout, request ID, recovery, custom structured logging, authentication).
-
Define Routes: Mounts the
oapi-codegengenerated handlers onto the router, applying the authentication middleware to protected routes. Defines public routes (login, signup, OAuth) and health checks separately. -
Start Server: Configures and starts the
http.Server. -
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.,*stringfor 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, likeFileStorageService. -
*_service.gofiles: Implement the service interfaces.-
Example (
todo_service.go):CreateTodovalidates input, potentially validates tag ownership viaTagService, callsTodoRepository.Create, callsTodoRepository.SetTags, and returns the result.DeleteTodocallsTodoRepository.Deleteand then potentially callsFileStorageService.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 theFileStorageServiceinterface, 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 theRepositoryRegistrywhich simplifies dependency injection by bundling all repository implementations. -
db.go: Helper function to establish thepgxpoolconnection pool. -
*_repo.gofiles: 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 thesqlc-generated structs and theinternal/domainstructs. -
They handle database-specific error mapping (e.g., converting
pgx.ErrNoRowstodomain.ErrNotFound, checking for unique constraint violations (23505) to returndomain.ErrConflict).
-
-
*_repo_cache.gofiles: 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.Cacheinstance. -
For read operations (
GetByID), they check the cache first. On a miss, they callnext.GetByID, store the result in the cache, and then return it. -
For write operations (
Create,Update,Delete), they callnext.*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 withsqlcannotations (-- name: CreateUser :one). This is where you define your database interactions. -
generated/*.go: Go code automatically generated bysqlcbased 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_*.gofiles: Generated byoapi-codegenfromopenapi.yaml. Includes:-
openapi_types.go: Go structs representing the API request/response models defined in the OpenAPI spec. -
openapi_generated.go: Defines theServerInterface(which handlers must implement) and the Chi router setup function (HandlerWithOptions) that connects URL paths to handler methods.
-
-
handlers.go: Implements theServerInterface.-
Each handler function typically:
-
Extracts the
userIDfrom the context (set by middleware). -
Parses and validates path parameters or the request body (using
parseAndValidateBodyhelper and potentially mapping API models to service input DTOs). -
Calls the appropriate method on a service from the
ServiceRegistry. -
Handles errors returned by the service, mapping them to appropriate HTTP status codes using
SendJSONError. -
Maps the domain object(s) returned by the service to the API response model(s) (e.g.,
mapDomainTodoToApi). -
Sends the successful JSON response using
SendJSONResponse.
-
-
-
middleware.go: Contains custom HTTP middleware.-
AuthMiddleware: Checks for JWT in header/cookie, validates it usingAuthService, and injects theuserIDinto 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 customClaimsstruct used for JWTs. -
oauth.go: Implements theOAuthProviderinterface 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 thestateparameter used in the OAuth flow to prevent CSRF attacks.
internal/cache
-
Purpose: Provides caching capabilities.
-
cache.go: Defines theCacheinterface (Driven Port) and thememoryCacheimplementation (Driven Adapter) usingpatrickmn/go-cache.
internal/config
-
Purpose: Handles loading and accessing application configuration.
-
config.go: Defines structs mirroring theconfig.yamlstructure 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 anup.sql(apply change) anddown.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 byoapi-codegento generate code and can be used to generate API documentation or client SDKs. -
sqlc.yaml: Configuressqlcregarding 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.
-
pgxDriver: Preferred native Go driver for PostgreSQL, known for better performance and features compared todatabase/sqlwrappers overlib/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 theCacheinterface 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)
-
Frontend: Sends POST request to
/api/v1/auth/loginwith email/password JSON body. -
Backend (
main.go/ Chi): Routes the request toApiHandler.LoginUserApi. -
handlers.go(LoginUserApi): Parses the JSON body intomodels.LoginRequest. CallsAuthService.Login. -
auth_service.go(Login): Validates input. CallsUserRepository.GetByEmail. Compares the hashed password usingbcrypt. If valid, callsAuthService.GenerateJWT. -
auth_service.go(GenerateJWT): Creates JWT claims (including UserID, expiry) and signs the token using the configured secret. -
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). -
Frontend: Receives the response. Stores the token (if needed) or relies on the cookie for subsequent requests. Updates auth state (via Zustand).
Create Todo
-
Frontend: Sends POST request to
/api/v1/todoswith Todo details (title, description, tags, etc.) in the JSON body and the JWT (via cookie or Authorization header). -
Backend (
main.go/ Chi): Routes the request.AuthMiddlewarevalidates the JWT and injects theuserIDinto the request context. The router callsApiHandler.CreateTodo. -
handlers.go(CreateTodo): ExtractsuserIDfrom context. Parses JSON body intomodels.CreateTodoRequest. Maps API model toservice.CreateTodoInput. CallsTodoService.CreateTodo. -
todo_service.go(CreateTodo): Validates input. IfTagIDsare present, callsTagService.ValidateUserTags. CallsTodoRepository.Createto insert the main todo data. If tags were provided, callsTodoRepository.SetTags(which likely runs in a transaction to clear old and add new tag associations). -
repository/todo_repo.go(Create,SetTags): Usessqlc-generated functions to execute the corresponding SQL INSERT and DELETE/INSERT operations against the PostgreSQL database. -
todo_service.go(CreateTodo): Returns the createddomain.Todoobject. -
handlers.go(CreateTodo): Receives the domain Todo. Maps it tomodels.Todo. Sends a 201 Created JSON response. -
Frontend: Receives the response. Updates UI state (e.g., via React Query invalidation/update).