Building a Go App with Supabase (Clean Architecture)
Learn how to build a production-ready Go application with Supabase using Clean Architecture principles and a Dependency Injection Container pattern.
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:
-- Create tasks table
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Row Level Security (RLS)
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Create policy to allow all operations (for demo purposes)
CREATE POLICY "Allow all operations" ON tasks
FOR ALL
USING (true)
WITH CHECK (true);
Step 3: Initialize Go Project
# Create project directory
mkdir go-supabase-demo
cd go-supabase-demo
# Initialize Go module
go mod init github.com/yourusername/go-supabase-demo
# Install dependencies
go get github.com/supabase-community/supabase-go
go get github.com/joho/godotenv
go get github.com/google/uuid
๐ Project Structure
go-supabase-demo/
โโโ cmd/
โ โโโ api/
โ โโโ main.go # Application entry point
โโโ internal/
โ โโโ domain/
โ โ โโโ task.go # Business entities
โ โโโ repository/
โ โ โโโ task_repository.go # Data access layer
โ โโโ usecase/
โ โ โโโ task_usecase.go # Business logic
โ โโโ handler/
โ โโโ task_handler.go # HTTP handlers
โโโ pkg/
โ โโโ container/
โ โ โโโ container.go # Dependency injection container
โ โโโ database/
โ โโโ supabase.go # Supabase client setup
โโโ .env # Environment variables
โโโ go.mod # Go dependencies
โโโ go.sum # Dependency checksums
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:
main.go
โ
โผ
โโโโโโโโโโโโโโโโโโโโ
โ Container โ โ Dependency Injection
โ (Wires all โ
โ dependencies) โ
โโโโโโโโโโฌโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Handler Layer โ โ HTTP/API Layer
โ (Receives requests, returns โ
โ responses) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UseCase Layer โ โ Business Logic
โ (Orchestrates operations, โ
โ enforces business rules) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Repository Layer โ โ Data Access
โ (Talks to database/external โ
โ services) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Domain Layer โ โ Core Entities
โ (Business entities/models) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
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:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key-here
PORT=8080
Step 5: Domain Layer (Entities)
Create internal/domain/task.go:
package domain
import (
"time"
"github.com/google/uuid"
)
// Task represents our core business entity
// This is framework-agnostic and contains only business data
type Task struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description *string `json:"description"` // Pointer because it's nullable
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateTaskDTO is used when creating a new task
// DTO = Data Transfer Object
type CreateTaskDTO struct {
Title string `json:"title"`
Description *string `json:"description,omitempty"`
}
// UpdateTaskDTO is used when updating a task
type UpdateTaskDTO struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Completed *bool `json:"completed,omitempty"`
}
// Validate checks if CreateTaskDTO has valid data
func (dto CreateTaskDTO) Validate() error {
if dto.Title == "" {
return ErrInvalidInput
}
return nil
}
Create internal/domain/errors.go:
package domain
import "errors"
// Define domain-specific errors
var (
ErrTaskNotFound = errors.New("task not found")
ErrInvalidInput = errors.New("invalid input")
)
Step 6: Repository Layer (Data Access)
Create internal/repository/task_repository.go:
package repository
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
supabase "github.com/supabase-community/supabase-go"
"github.com/yourusername/go-supabase-demo/internal/domain"
)
// TaskRepository defines the interface for task data operations
// Using an interface allows us to swap implementations easily
type TaskRepository interface {
Create(ctx context.Context, task *domain.CreateTaskDTO) (*domain.Task, error)
GetByID(ctx context.Context, id uuid.UUID) (*domain.Task, error)
GetAll(ctx context.Context) ([]domain.Task, error)
Update(ctx context.Context, id uuid.UUID, dto *domain.UpdateTaskDTO) (*domain.Task, error)
Delete(ctx context.Context, id uuid.UUID) error
}
// supabaseTaskRepository implements TaskRepository using Supabase
type supabaseTaskRepository struct {
client *supabase.Client
}
// NewTaskRepository creates a new task repository
func NewTaskRepository(client *supabase.Client) TaskRepository {
return &supabaseTaskRepository{
client: client,
}
}
// Create inserts a new task into the database
func (r *supabaseTaskRepository) Create(ctx context.Context, dto *domain.CreateTaskDTO) (*domain.Task, error) {
// Prepare data for insertion
data := map[string]interface{}{
"title": dto.Title,
"description": dto.Description,
"completed": false,
}
// Execute insert query
result, _, err := r.client.From("tasks").Insert(data, false, "", "", "").Execute()
if err != nil {
return nil, fmt.Errorf("failed to create task: %w", err)
}
// Parse response
var tasks []domain.Task
if err := json.Unmarshal(result, &tasks); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if len(tasks) == 0 {
return nil, fmt.Errorf("no task returned after creation")
}
return &tasks[0], nil
}
// GetByID retrieves a single task by its ID
func (r *supabaseTaskRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Task, error) {
result, _, err := r.client.From("tasks").
Select("*", "exact", false).
Eq("id", id.String()).
Single().
Execute()
if err != nil {
return nil, fmt.Errorf("failed to get task: %w", err)
}
var task domain.Task
if err := json.Unmarshal(result, &task); err != nil {
return nil, fmt.Errorf("failed to parse task: %w", err)
}
return &task, nil
}
// GetAll retrieves all tasks
func (r *supabaseTaskRepository) GetAll(ctx context.Context) ([]domain.Task, error) {
result, _, err := r.client.From("tasks").
Select("*", "exact", false).
Order("created_at", &supabase.OrderOpts{Ascending: false}).
Execute()
if err != nil {
return nil, fmt.Errorf("failed to get tasks: %w", err)
}
var tasks []domain.Task
if err := json.Unmarshal(result, &tasks); err != nil {
return nil, fmt.Errorf("failed to parse tasks: %w", err)
}
return tasks, nil
}
// Update modifies an existing task
func (r *supabaseTaskRepository) Update(ctx context.Context, id uuid.UUID, dto *domain.UpdateTaskDTO) (*domain.Task, error) {
// Build update data (only include non-nil fields)
data := make(map[string]interface{})
if dto.Title != nil {
data["title"] = *dto.Title
}
if dto.Description != nil {
data["description"] = *dto.Description
}
if dto.Completed != nil {
data["completed"] = *dto.Completed
}
data["updated_at"] = "now()"
result, _, err := r.client.From("tasks").
Update(data, "", "").
Eq("id", id.String()).
Execute()
if err != nil {
return nil, fmt.Errorf("failed to update task: %w", err)
}
var tasks []domain.Task
if err := json.Unmarshal(result, &tasks); err != nil {
return nil, fmt.Errorf("failed to parse updated task: %w", err)
}
if len(tasks) == 0 {
return nil, domain.ErrTaskNotFound
}
return &tasks[0], nil
}
// Delete removes a task from the database
func (r *supabaseTaskRepository) Delete(ctx context.Context, id uuid.UUID) error {
_, _, err := r.client.From("tasks").
Delete("", "").
Eq("id", id.String()).
Execute()
if err != nil {
return fmt.Errorf("failed to delete task: %w", err)
}
return nil
}
Step 7: UseCase Layer (Business Logic)
Create internal/usecase/task_usecase.go:
package usecase
import (
"context"
"github.com/google/uuid"
"github.com/yourusername/go-supabase-demo/internal/domain"
"github.com/yourusername/go-supabase-demo/internal/repository"
)
// TaskUseCase defines business operations for tasks
type TaskUseCase interface {
CreateTask(ctx context.Context, dto *domain.CreateTaskDTO) (*domain.Task, error)
GetTask(ctx context.Context, id uuid.UUID) (*domain.Task, error)
ListTasks(ctx context.Context) ([]domain.Task, error)
UpdateTask(ctx context.Context, id uuid.UUID, dto *domain.UpdateTaskDTO) (*domain.Task, error)
DeleteTask(ctx context.Context, id uuid.UUID) error
}
// taskUseCase implements TaskUseCase
type taskUseCase struct {
taskRepo repository.TaskRepository
}
// NewTaskUseCase creates a new task use case
func NewTaskUseCase(taskRepo repository.TaskRepository) TaskUseCase {
return &taskUseCase{
taskRepo: taskRepo,
}
}
// CreateTask creates a new task with validation
func (uc *taskUseCase) CreateTask(ctx context.Context, dto *domain.CreateTaskDTO) (*domain.Task, error) {
// Validate input
if err := dto.Validate(); err != nil {
return nil, err
}
// Business logic: You could add more rules here
// For example: check user permissions, apply business rules, etc.
// Delegate to repository
return uc.taskRepo.Create(ctx, dto)
}
// GetTask retrieves a task by ID
func (uc *taskUseCase) GetTask(ctx context.Context, id uuid.UUID) (*domain.Task, error) {
return uc.taskRepo.GetByID(ctx, id)
}
// ListTasks retrieves all tasks
func (uc *taskUseCase) ListTasks(ctx context.Context) ([]domain.Task, error) {
return uc.taskRepo.GetAll(ctx)
}
// UpdateTask updates a task
func (uc *taskUseCase) UpdateTask(ctx context.Context, id uuid.UUID, dto *domain.UpdateTaskDTO) (*domain.Task, error) {
// You could add validation here
return uc.taskRepo.Update(ctx, id, dto)
}
// DeleteTask deletes a task
func (uc *taskUseCase) DeleteTask(ctx context.Context, id uuid.UUID) error {
return uc.taskRepo.Delete(ctx, id)
}
Step 8: Handler Layer (HTTP API)
Create internal/handler/task_handler.go:
package handler
import (
"encoding/json"
"net/http"
"github.com/google/uuid"
"github.com/yourusername/go-supabase-demo/internal/domain"
"github.com/yourusername/go-supabase-demo/internal/usecase"
)
// TaskHandler handles HTTP requests for tasks
type TaskHandler struct {
taskUseCase usecase.TaskUseCase
}
// NewTaskHandler creates a new task handler
func NewTaskHandler(taskUseCase usecase.TaskUseCase) *TaskHandler {
return &TaskHandler{
taskUseCase: taskUseCase,
}
}
// Response is a generic API response wrapper
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// CreateTask handles POST /tasks
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
var dto domain.CreateTaskDTO
if err := json.NewDecoder(r.Body).Decode(&dto); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
task, err := h.taskUseCase.CreateTask(r.Context(), &dto)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusCreated, task)
}
// GetTask handles GET /tasks/{id}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL path
idStr := r.PathValue("id")
id, err := uuid.Parse(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid task ID")
return
}
task, err := h.taskUseCase.GetTask(r.Context(), id)
if err != nil {
respondError(w, http.StatusNotFound, err.Error())
return
}
respondJSON(w, http.StatusOK, task)
}
// ListTasks handles GET /tasks
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := h.taskUseCase.ListTasks(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, tasks)
}
// UpdateTask handles PUT /tasks/{id}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := uuid.Parse(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid task ID")
return
}
var dto domain.UpdateTaskDTO
if err := json.NewDecoder(r.Body).Decode(&dto); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
task, err := h.taskUseCase.UpdateTask(r.Context(), id, &dto)
if err != nil {
respondError(w, http.StatusBadRequest, err.Error())
return
}
respondJSON(w, http.StatusOK, task)
}
// DeleteTask handles DELETE /tasks/{id}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := uuid.Parse(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid task ID")
return
}
if err := h.taskUseCase.DeleteTask(r.Context(), id); err != nil {
respondError(w, http.StatusInternalServerError, err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "Task deleted successfully"})
}
// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Success: true,
Data: data,
})
}
func respondError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(Response{
Success: false,
Error: message,
})
}
Step 9: Supabase Client Setup
Create pkg/database/supabase.go:
package database
import (
"fmt"
"os"
supabase "github.com/supabase-community/supabase-go"
)
// NewSupabaseClient creates and returns a new Supabase client
func NewSupabaseClient() (*supabase.Client, error) {
supabaseURL := os.Getenv("SUPABASE_URL")
supabaseKey := os.Getenv("SUPABASE_KEY")
if supabaseURL == "" || supabaseKey == "" {
return nil, fmt.Errorf("SUPABASE_URL and SUPABASE_KEY must be set")
}
client, err := supabase.NewClient(supabaseURL, supabaseKey, &supabase.ClientOptions{})
if err != nil {
return nil, fmt.Errorf("failed to initialize Supabase client: %w", err)
}
return client, nil
}
Step 10: Dependency Injection Container
Create pkg/container/container.go:
package container
import (
"sync"
supabase "github.com/supabase-community/supabase-go"
"github.com/yourusername/go-supabase-demo/internal/handler"
"github.com/yourusername/go-supabase-demo/internal/repository"
"github.com/yourusername/go-supabase-demo/internal/usecase"
)
// Container holds all application dependencies
// This pattern centralizes dependency management and makes testing easier
type Container struct {
// Database client
supabaseClient *supabase.Client
// Repository layer (lazy initialized)
taskRepo repository.TaskRepository
taskRepoOnce sync.Once
// UseCase layer (lazy initialized)
taskUseCase usecase.TaskUseCase
taskUseCaseOnce sync.Once
// Handler layer (lazy initialized)
taskHandler *handler.TaskHandler
taskHandlerOnce sync.Once
}
// NewContainer creates a new dependency injection container
func NewContainer(supabaseClient *supabase.Client) *Container {
return &Container{
supabaseClient: supabaseClient,
}
}
// GetTaskRepository returns the task repository instance (singleton)
// sync.Once ensures it's only created once, even with concurrent calls
func (c *Container) GetTaskRepository() repository.TaskRepository {
c.taskRepoOnce.Do(func() {
c.taskRepo = repository.NewTaskRepository(c.supabaseClient)
})
return c.taskRepo
}
// GetTaskUseCase returns the task use case instance (singleton)
func (c *Container) GetTaskUseCase() usecase.TaskUseCase {
c.taskUseCaseOnce.Do(func() {
c.taskUseCase = usecase.NewTaskUseCase(c.GetTaskRepository())
})
return c.taskUseCase
}
// GetTaskHandler returns the task handler instance (singleton)
func (c *Container) GetTaskHandler() *handler.TaskHandler {
c.taskHandlerOnce.Do(func() {
c.taskHandler = handler.NewTaskHandler(c.GetTaskUseCase())
})
return c.taskHandler
}
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:
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
"github.com/yourusername/go-supabase-demo/pkg/container"
"github.com/yourusername/go-supabase-demo/pkg/database"
)
func main() {
// Load environment variables from .env file
if err := godotenv.Load(); err != nil {
log.Println("No .env file found, using environment variables")
}
// Initialize Supabase client
supabaseClient, err := database.NewSupabaseClient()
if err != nil {
log.Fatal("Failed to initialize Supabase client:", err)
}
// Initialize dependency injection container
// The container manages all dependencies and their lifecycles
appContainer := container.NewContainer(supabaseClient)
// Get handlers from container (dependencies are auto-wired)
taskHandler := appContainer.GetTaskHandler()
// Setup HTTP routes using Go 1.22+ pattern matching
mux := http.NewServeMux()
// Task routes
mux.HandleFunc("GET /tasks", taskHandler.ListTasks)
mux.HandleFunc("POST /tasks", taskHandler.CreateTask)
mux.HandleFunc("GET /tasks/{id}", taskHandler.GetTask)
mux.HandleFunc("PUT /tasks/{id}", taskHandler.UpdateTask)
mux.HandleFunc("DELETE /tasks/{id}", taskHandler.DeleteTask)
// Health check endpoint
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// Get port from environment or use default
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// Start server
addr := fmt.Sprintf(":%s", port)
log.Printf("๐ Server starting on http://localhost%s", addr)
log.Printf("๐ API endpoints:")
log.Printf(" GET /health")
log.Printf(" GET /tasks")
log.Printf(" POST /tasks")
log.Printf(" GET /tasks/{id}")
log.Printf(" PUT /tasks/{id}")
log.Printf(" DELETE /tasks/{id}")
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal("Failed to start server:", err)
}
}
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
go mod tidy
3. Set Environment Variables
Make sure your .env file has correct values:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key
PORT=8080
4. Run the Application
go run cmd/api/main.go
You should see:
๐ Server starting on http://localhost:8080
๐ API endpoints:
GET /health
GET /tasks
POST /tasks
...
๐งช Testing the API
Create a Task
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Learn Go",
"description": "Study clean architecture with Supabase"
}'
Get All Tasks
curl http://localhost:8080/tasks
Get Single Task
curl http://localhost:8080/tasks/{task-id}
Update a Task
curl -X PUT http://localhost:8080/tasks/{task-id} \
-H "Content-Type: application/json" \
-d '{
"completed": true
}'
Delete a Task
curl -X DELETE http://localhost:8080/tasks/{task-id}
๐ 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
// Container pattern centralizes dependency management
appContainer := container.NewContainer(supabaseClient)
taskHandler := appContainer.GetTaskHandler() // โ Auto-wires all dependencies
// Behind the scenes, the container builds:
// taskRepo โ taskUseCase โ taskHandler
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
// In tests, you can create a test container:
mockRepo := &MockTaskRepository{}
testContainer := &container.Container{
taskRepo: mockRepo,
}
handler := testContainer.GetTaskHandler()
// Now handler uses your mock!
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)
var instance *MyService
var once sync.Once
func GetInstance() *MyService {
once.Do(func() {
instance = &MyService{} // Only runs once, even with 1000 concurrent calls
})
return instance
}
- Ensures initialization happens exactly once
- Thread-safe without manual locking
- Used in our container for singleton pattern
6. Error Handling
if err != nil {
return nil, fmt.Errorf("descriptive message: %w", err)
}
- 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! ๐