Complete Guide: Building a Go App with Supabase (Clean Architecture)
๐ Table of Contents
- Project Setup
- Project Structure
- Understanding Clean Architecture
- Step-by-Step Implementation
- 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
- Go to https://supabase.com
- Create a new project
- Wait for database provisioning
- Go to Settings โ API and note:
- Project URL (e.g.,
https://xxxxx.supabase.co) anonpublic key (starts witheyJ...)
- Project URL (e.g.,
Step 2: Create Database Table
- In Supabase dashboard, go to SQL Editor
- Run this SQL:
Step 3: Initialize Go Project
๐ Project Structure
Why this structure?
cmd/: Entry points for different applicationsinternal/: 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.Onceensures 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.gointernal/usecase/task_usecase.gointernal/handler/task_handler.gopkg/container/container.gocmd/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
nilmeans "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
%wto wrap errors for better debugging
๐ Next Steps
- Add Authentication: Use Supabase Auth (
client.SignInWithEmailPassword) - Add Middleware: Logging, CORS, authentication
- Add Tests: Unit tests for each layer
- Add Validation: Use a library like
go-playground/validator - Add Documentation: Use Swagger/OpenAPI
- Containerize: Create a Dockerfile
๐ Additional Resources
Happy coding! ๐