Building a Go App with Supabase (Clean Architecture)

Complete Guide: Building a Go App with Supabase (Clean Architecture)

๐Ÿ“š Table of Contents

  1. Project Setup
  2. Project Structure
  3. Understanding Clean Architecture
  4. Step-by-Step Implementation
  5. Running the Application

๐Ÿš€ Project Setup

Prerequisites

  • Go 1.21+ installed
  • Supabase account (free tier is fine)
  • Basic terminal/command line knowledge

Step 1: Create Supabase Project

  1. Go to https://supabase.com
  2. Create a new project
  3. Wait for database provisioning
  4. Go to Settings โ†’ API and note:
    • Project URL (e.g., https://xxxxx.supabase.co)
    • anon public key (starts with eyJ...)

Step 2: Create Database Table

  1. In Supabase dashboard, go to SQL Editor
  2. Run this SQL:

Step 3: Initialize Go Project


๐Ÿ“ Project Structure

Why this structure?

  • cmd/: Entry points for different applications
  • internal/: Private application code (can't be imported by other projects)
  • pkg/: Public libraries (can be imported by other projects)
  • pkg/container/: Centralized dependency management
  • Each layer has a single responsibility

๐Ÿ—๏ธ Understanding Clean Architecture

Clean Architecture separates concerns into layers:

Benefits:

  • Testable: Each layer can be tested independently
  • Maintainable: Changes in one layer don't affect others
  • Scalable: Easy to add new features
  • Database-agnostic: Can swap Supabase for another DB easily
  • Clean Dependency Flow: Container manages all wiring in one place

๐Ÿ”จ Step-by-Step Implementation

Step 4: Create Environment File

Create .env in project root:

Step 5: Domain Layer (Entities)

Create internal/domain/task.go:

Create internal/domain/errors.go:

Step 6: Repository Layer (Data Access)

Create internal/repository/task_repository.go:

Step 7: UseCase Layer (Business Logic)

Create internal/usecase/task_usecase.go:

Step 8: Handler Layer (HTTP API)

Create internal/handler/task_handler.go:

Step 9: Supabase Client Setup

Create pkg/database/supabase.go:

Step 10: Dependency Injection Container

Create pkg/container/container.go:

Why use a Container?

  • Centralized Dependency Management: All dependencies are managed in one place
  • Lazy Initialization: Dependencies are only created when needed
  • Thread-Safe: sync.Once ensures safe concurrent access
  • Easy Testing: Can mock dependencies by creating test containers
  • Scalability: Easy to add new dependencies without changing main.go

Step 11: Main Application Entry Point

Create cmd/api/main.go:

What changed?

  • โœ… Removed manual dependency injection code
  • โœ… Added container initialization: appContainer := container.NewContainer(supabaseClient)
  • โœ… Get dependencies from container: taskHandler := appContainer.GetTaskHandler()
  • โœ… Much cleaner and more maintainable main function
  • โœ… Adding new dependencies only requires updating the container

โ–ถ๏ธ Running the Application

1. Update Import Paths

Replace github.com/yourusername/go-supabase-demo with your actual module name in:

  • internal/repository/task_repository.go
  • internal/usecase/task_usecase.go
  • internal/handler/task_handler.go
  • pkg/container/container.go
  • cmd/api/main.go

2. Install Dependencies

3. Set Environment Variables

Make sure your .env file has correct values:

4. Run the Application

You should see:


๐Ÿงช Testing the API

Create a Task

Get All Tasks

Get Single Task

Update a Task

Delete a Task


๐ŸŽ“ Key Concepts Explained

1. Interfaces (repository.TaskRepository, usecase.TaskUseCase)

  • Define contracts for behavior
  • Enable dependency injection
  • Make testing easier (can create mock implementations)

2. Dependency Injection Container

Benefits of the Container Pattern:

  • Single Responsibility: Main.go focuses on startup, not wiring
  • Lazy Loading: Dependencies created only when needed
  • Singleton Pattern: Each dependency created once via sync.Once
  • Easy Testing: Can create mock containers for unit tests
  • Scalability: Add new services without cluttering main.go

Example: Testing with Container

3. Pointers (e.g., *string, *domain.Task)

  • * means "pointer to"
  • Used for nullable fields or when you want to modify the original
  • nil means "no value"

4. Context (context.Context)

  • Carries deadlines, cancellation signals, and request-scoped values
  • Always pass as first parameter to functions
  • Used for request timeout/cancellation

5. sync.Once (Thread-Safe Initialization)

  • Ensures initialization happens exactly once
  • Thread-safe without manual locking
  • Used in our container for singleton pattern

6. Error Handling

  • Go doesn't have exceptions; functions return errors
  • Always check errors immediately
  • Use %w to wrap errors for better debugging

๐Ÿš€ Next Steps

  1. Add Authentication: Use Supabase Auth (client.SignInWithEmailPassword)
  2. Add Middleware: Logging, CORS, authentication
  3. Add Tests: Unit tests for each layer
  4. Add Validation: Use a library like go-playground/validator
  5. Add Documentation: Use Swagger/OpenAPI
  6. Containerize: Create a Dockerfile

๐Ÿ“š Additional Resources

Happy coding! ๐ŸŽ‰