ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-04-08Advanced

Antigravity × Go Backend API Masterclass: From High-Performance Microservice Design to Production Deployment

A complete masterclass on building production-grade Go backend APIs using Antigravity's AI capabilities with Echo, sqlc, and PostgreSQL — from architecture and implementation to testing and Cloud Run deployment.

gogolangbackend3api13echosqlcpostgresql5cloud-runmicroservices3

Setup and context: Why Go + Antigravity in 2026?

Antigravity has transformed the TypeScript/JavaScript development experience — but its benefits extend just as powerfully to Go. With its static typing, fast compilation, and elegant concurrency model, Go has seen explosive adoption in microservices and serverless environments, making it an essential skill for backend developers in 2026.

This article provides a complete, hands-on guide to building a production-grade Go RESTful API from scratch using Antigravity's Agent features, contextual understanding, and AI code completion. Rather than a toy tutorial, you'll learn the design philosophy and implementation patterns that hold up in real production environments.

Who This Is For

  • Go developers who have the basics down and want to level up to production-quality implementations
  • TypeScript/Node.js backend developers exploring a migration to Go
  • Developers who want to apply microservice design and API best practices with depth
  • Anyone looking to 10x their Go productivity using an AI IDE

What You'll Build

A RESTful API server with user management, authentication, and post management features, using the following stack:

  • Framework: Echo v4 (high-performance, minimal routing)
  • Database: PostgreSQL + sqlc (type-safe SQL query generation)
  • Auth: JWT (JSON Web Token)
  • Testing: testify + testcontainers-go
  • Deployment: Cloud Run (Google Cloud) or Fly.io
  • AI Assistance: Antigravity Agent, code completion, error diagnostics

Environment Setup and Antigravity Project Configuration

Installing Required Tools

# Install Go 1.22+ (via official site)
go version  # go version go1.22.x
 
# sqlc — type-safe SQL → Go code generator
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
 
# golang-migrate — DB migration manager
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
 
# air — hot reload for development
go install github.com/air-verse/air@latest

Setting Up AGENTS.md in Antigravity

Create an AGENTS.md file at the project root to give Antigravity Agent full context about your project. This is the single most impactful thing you can do to boost AI-assisted productivity.

# Project Context
 
## Overview
Go RESTful API server using Echo v4, sqlc, PostgreSQL.
This is a production-grade backend with JWT authentication.
 
## Architecture
- cmd/server/main.go — Entry point
- internal/api/ — HTTP handlers and routing
- internal/db/ — sqlc-generated code (do not edit manually)
- internal/service/ — Business logic layer
- internal/middleware/ — Custom middlewares
- db/migrations/ — SQL migration files
- db/query/ — sqlc query definitions
 
## Conventions
- Wrap errors with errors.Wrap to preserve stack traces
- Use slog (standard library) for structured logging
- Use testify assertions in tests
- Use testcontainers-go for DB tests with a real PostgreSQL instance
 
## Database
- PostgreSQL 16 (type-safe queries via sqlc)
- Migrations managed by golang-migrate
 
## Do not
- Manually edit files inside internal/db/ (generated by sqlc)
- Use global variables

With this AGENTS.md in place, Antigravity Agent understands your Go conventions, project structure, and naming patterns before generating any code.


Project Structure Design

Directory Layout

When you ask Antigravity Agent to "create the directory structure for this project," it proposes a layout aligned with Go standard conventions. Here's the recommended structure:

myapi/
├── AGENTS.md
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── api/
│   │   ├── handler/
│   │   │   ├── user_handler.go
│   │   │   └── post_handler.go
│   │   ├── middleware/
│   │   │   ├── auth.go
│   │   │   └── logger.go
│   │   └── router.go
│   ├── db/
│   │   └── (sqlc-generated files — do not edit)
│   └── service/
│       ├── user_service.go
│       └── post_service.go
├── db/
│   ├── migrations/
│   │   ├── 000001_create_users.up.sql
│   │   └── 000001_create_users.down.sql
│   └── query/
│       ├── users.sql
│       └── posts.sql
├── sqlc.yaml
├── go.mod
└── .air.toml

Initializing go.mod

mkdir myapi && cd myapi
go mod init github.com/yourname/myapi
 
# Add dependencies
go get github.com/labstack/echo/v4
go get github.com/labstack/echo-jwt/v4
go get github.com/golang-jwt/jwt/v5
go get github.com/jackc/pgx/v5
go get github.com/golang-migrate/migrate/v4
go get github.com/joho/godotenv
go get go.uber.org/zap

DB Schema Design and Type-Safe Queries with sqlc

Writing Migration Files

Ask Antigravity to "design an ERD for users and posts, and generate SQL in golang-migrate format." Review and adjust as needed.

-- db/migrations/000001_create_users.up.sql
CREATE TABLE users (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email       VARCHAR(255) NOT NULL UNIQUE,
    username    VARCHAR(50)  NOT NULL UNIQUE,
    password    VARCHAR(255) NOT NULL,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);
 
CREATE INDEX idx_users_email    ON users (email);
CREATE INDEX idx_users_username ON users (username);
 
-- db/migrations/000002_create_posts.up.sql
CREATE TABLE posts (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID         NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title       VARCHAR(200) NOT NULL,
    content     TEXT         NOT NULL,
    published   BOOLEAN      NOT NULL DEFAULT FALSE,
    created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);
 
CREATE INDEX idx_posts_user_id ON posts (user_id);

sqlc.yaml Configuration

# sqlc.yaml
version: "2"
sql:
  - engine: "postgresql"
    queries: "db/query/"
    schema: "db/migrations/"
    gen:
      go:
        package: "db"
        out: "internal/db"
        emit_json_tags: true
        emit_prepared_queries: true
        emit_interface: true
        emit_exact_table_names: false

Defining SQL Queries

-- db/query/users.sql
 
-- name: CreateUser :one
INSERT INTO users (email, username, password)
VALUES ($1, $2, $3)
RETURNING *;
 
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
 
-- name: GetUserByID :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
 
-- name: ListUsers :many
SELECT id, email, username, created_at FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
# Generate type-safe Go code from SQL
sqlc generate
# → Type-safe Go code is generated into internal/db/

Once these generated files are in your project, you can tell Antigravity "write a user creation handler using db.CreateUserParams" and it will generate accurate, type-checked code every time. This is the true power of sqlc + Antigravity.


Building the API with Echo

Entry Point (cmd/server/main.go)

package main
 
import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
 
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/joho/godotenv"
    "github.com/yourname/myapi/internal/api"
)
 
func main() {
    _ = godotenv.Load()
 
    // Structured logger setup
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    slog.SetDefault(logger)
 
    // DB connection pool
    pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
    if err != nil {
        slog.Error("Failed to connect to database", "error", err)
        os.Exit(1)
    }
    defer pool.Close()
 
    // Start Echo server
    e := api.NewRouter(pool)
    srv := &http.Server{
        Addr:         ":" + os.Getenv("PORT"),
        Handler:      e,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
 
    // Graceful shutdown
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            slog.Error("Server error", "error", err)
        }
    }()
 
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
 
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    _ = srv.Shutdown(ctx)
    slog.Info("Server shut down gracefully")
}

Router Configuration (internal/api/router.go)

package api
 
import (
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
    echojwt "github.com/labstack/echo-jwt/v4"
    "github.com/yourname/myapi/internal/api/handler"
    mw "github.com/yourname/myapi/internal/middleware"
)
 
func NewRouter(pool *pgxpool.Pool) *echo.Echo {
    e := echo.New()
 
    // Global middleware
    e.Use(middleware.RequestID())
    e.Use(mw.StructuredLogger())
    e.Use(middleware.Recover())
    e.Use(middleware.CORS())
 
    // Initialize handlers
    userH := handler.NewUserHandler(pool)
    postH := handler.NewPostHandler(pool)
 
    // Public endpoints
    v1 := e.Group("/api/v1")
    v1.POST("/auth/register", userH.Register)
    v1.POST("/auth/login",    userH.Login)
 
    // Authenticated endpoints
    auth := v1.Group("")
    auth.Use(echojwt.WithConfig(echojwt.Config{
        SigningKey: []byte(os.Getenv("JWT_SECRET")),
    }))
    auth.GET("/users/me",        userH.GetMe)
    auth.GET("/posts",           postH.List)
    auth.POST("/posts",          postH.Create)
    auth.GET("/posts/:id",       postH.Get)
    auth.PUT("/posts/:id",       postH.Update)
    auth.DELETE("/posts/:id",    postH.Delete)
 
    return e
}

JWT Authentication and User Handler

Ask Antigravity: "Write a JWT login handler using sqlc's GetUserByEmail query — verify the password with bcrypt and return a signed JWT." Here's what gets generated:

// internal/api/handler/user_handler.go (excerpt)
func (h *UserHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
    }
    if err := c.Validate(req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
 
    // Fetch user from DB (sqlc-generated code)
    user, err := h.queries.GetUserByEmail(c.Request().Context(), req.Email)
    if err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
    }
 
    // Verify bcrypt password
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
    }
 
    // Generate JWT token
    claims := &JWTClaims{
        UserID:   user.ID.String(),
        Username: user.Username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    signed, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
    }
 
    return c.JSON(http.StatusOK, LoginResponse{Token: signed})
}

Key Antigravity tip: Because AGENTS.md describes the sqlc type schema, Antigravity generates handlers that correctly reference db.User field names — eliminating the type mismatch errors that slow down development when starting a new Go project.


Validation and Error Handling

Custom Validator

// internal/api/validator.go
package api
 
import (
    "net/http"
    "github.com/go-playground/validator/v10"
    "github.com/labstack/echo/v4"
)
 
type CustomValidator struct {
    validator *validator.Validate
}
 
func NewCustomValidator() *CustomValidator {
    return &CustomValidator{validator: validator.New()}
}
 
func (cv *CustomValidator) Validate(i interface{}) error {
    if err := cv.validator.Struct(i); err != nil {
        errs := err.(validator.ValidationErrors)
        messages := make([]string, len(errs))
        for i, e := range errs {
            messages[i] = fmt.Sprintf("%s: %s", e.Field(), e.Tag())
        }
        return echo.NewHTTPError(http.StatusBadRequest, strings.Join(messages, ", "))
    }
    return nil
}

Global Error Handler

// internal/middleware/error_handler.go
func CustomHTTPErrorHandler(err error, c echo.Context) {
    code := http.StatusInternalServerError
    message := "An internal server error occurred"
 
    var he *echo.HTTPError
    if errors.As(err, &he) {
        code = he.Code
        if msg, ok := he.Message.(string); ok {
            message = msg
        }
    }
 
    // Log full details only for 5xx errors
    if code >= 500 {
        slog.Error("HTTP error", "code", code, "error", err,
            "path", c.Request().URL.Path,
            "request_id", c.Response().Header().Get(echo.HeaderXRequestID),
        )
    }
 
    c.JSON(code, map[string]interface{}{
        "error":      message,
        "request_id": c.Response().Header().Get(echo.HeaderXRequestID),
    })
}

Integration Testing with testcontainers-go

The most reliable way to ensure production quality in Go is integration testing with a real database. Ask Antigravity: "Generate a PostgreSQL integration test setup using testcontainers-go." The result looks like this:

// internal/service/user_service_test.go
package service_test
 
import (
    "context"
    "testing"
 
    "github.com/jackc/pgx/v5/pgxpool"
    "github.com/stretchr/testify/require"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/yourname/myapi/internal/db"
    "github.com/yourname/myapi/internal/service"
)
 
func setupTestDB(t *testing.T) *pgxpool.Pool {
    t.Helper()
 
    // Spin up a real PostgreSQL container
    pgContainer, err := postgres.RunContainer(
        context.Background(),
        testcontainers.WithImage("postgres:16"),
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second),
        ),
    )
    require.NoError(t, err)
    t.Cleanup(func() { pgContainer.Terminate(context.Background()) })
 
    connStr, err := pgContainer.ConnectionString(context.Background(), "sslmode=disable")
    require.NoError(t, err)
 
    // Run migrations
    m, err := migrate.New("file://../../db/migrations", connStr)
    require.NoError(t, err)
    require.NoError(t, m.Up())
 
    pool, err := pgxpool.New(context.Background(), connStr)
    require.NoError(t, err)
    t.Cleanup(pool.Close)
 
    return pool
}
 
func TestUserService_CreateUser(t *testing.T) {
    pool := setupTestDB(t)
    svc := service.NewUserService(db.New(pool))
 
    user, err := svc.CreateUser(context.Background(), service.CreateUserParams{
        Email:    "test@example.com",
        Username: "testuser",
        Password: "securepassword123",
    })
 
    require.NoError(t, err)
    require.Equal(t, "test@example.com", user.Email)
    require.NotEmpty(t, user.ID)
    // Password must be hashed, not stored in plain text
    require.NotEqual(t, "securepassword123", user.Password)
}

Running the tests (allow 2–3 minutes on first run while Docker images are pulled):

$ go test ./... -v -count=1
 
--- PASS: TestUserService_CreateUser (2.34s)
--- PASS: TestUserService_GetUserByEmail (0.12s)
--- PASS: TestPostService_CreateAndPublish (0.89s)
PASS
ok      github.com/yourname/myapi/internal/service  3.35s

Deploying to Cloud Run

Dockerfile with Multi-Stage Build

# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
 
# Cache dependency layer
COPY go.mod go.sum ./
RUN go mod download
 
# Build a single static binary (CGO disabled)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w" \
    -o /api ./cmd/server
 
# Minimal distroless runtime image (security-hardened)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]

Cloud Run Deployment

# Authenticate and push to Artifact Registry
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
 
docker build -t asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest .
docker push asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest
 
# Deploy to Cloud Run (zero-downtime rolling update)
gcloud run deploy myapi \
  --image asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest \
  --region asia-northeast1 \
  --platform managed \
  --allow-unauthenticated \
  --min-instances 1 \
  --max-instances 10 \
  --concurrency 100 \
  --set-env-vars "DATABASE_URL=postgres://...,JWT_SECRET=YOUR_JWT_SECRET"

Automating CI/CD with GitHub Actions

Ask Antigravity Agent to "create a GitHub Actions workflow for automated Cloud Run deployment." It will generate a complete pipeline covering test → build → deploy. For the full implementation details, see our GitHub Actions Advanced CI/CD Pipeline Guide.


Performance Optimization with Antigravity

Setting Up pprof Profiling

// Add to main.go (guard with authentication in production)
import _ "net/http/pprof"
 
go func() {
    slog.Info("pprof server listening", "addr", "localhost:6060")
    http.ListenAndServe("localhost:6060", nil)
}()
# Capture a 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
 
# Then ask Antigravity:
# "Review this profile output and identify bottleneck functions with improvement suggestions"

DB Query Optimization with Antigravity

When sqlc query files are included in Antigravity's context, prompts like "suggest indexes that would improve this query and explain why" return data-driven recommendations based on the actual query structure:

-- Example optimization suggested by Antigravity based on query plan analysis
EXPLAIN ANALYZE
SELECT p.* FROM posts p
WHERE p.user_id = $1 AND p.published = true
ORDER BY p.created_at DESC;
 
-- Composite index addition
CREATE INDEX CONCURRENTLY idx_posts_user_published
ON posts (user_id, published, created_at DESC);
-- Execution time: 120ms → 3ms (97.5% improvement)

Rate Limiting, Security Hardening, and Middleware Best Practices

Rate Limiting with Redis

Production APIs need rate limiting to protect against abuse. Ask Antigravity: "Add Redis-based rate limiting middleware to this Echo API using a sliding window algorithm."

// internal/middleware/rate_limiter.go
package middleware
 
import (
    "context"
    "fmt"
    "net/http"
    "time"
 
    "github.com/labstack/echo/v4"
    "github.com/redis/go-redis/v9"
)
 
// RateLimiter implements a sliding window rate limiter backed by Redis.
// This approach correctly handles distributed deployments (multiple Cloud Run instances).
func RateLimiter(rdb *redis.Client, limit int, window time.Duration) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            key := fmt.Sprintf("rl:%s:%s", c.RealIP(), c.Request().URL.Path)
            ctx := context.Background()
 
            pipe := rdb.Pipeline()
            incr := pipe.Incr(ctx, key)
            pipe.Expire(ctx, key, window)
            _, err := pipe.Exec(ctx)
            if err != nil {
                // Fail open: if Redis is unavailable, allow the request
                return next(c)
            }
 
            count := incr.Val()
            c.Response().Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit))
            c.Response().Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", max(0, int64(limit)-count)))
 
            if count > int64(limit) {
                return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded. Please slow down.")
            }
            return next(c)
        }
    }
}

Security Headers Middleware

// internal/middleware/security.go
func SecurityHeaders() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            h := c.Response().Header()
            h.Set("X-Content-Type-Options", "nosniff")
            h.Set("X-Frame-Options", "DENY")
            h.Set("X-XSS-Protection", "1; mode=block")
            h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
            h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
            h.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
            return next(c)
        }
    }
}

Input Sanitization for SQL Injection Prevention

sqlc's parameterized queries prevent SQL injection by design, but it's worth understanding why. When Antigravity generates sqlc queries, every $1, $2 placeholder is sent as a separate parameter over the PostgreSQL wire protocol — the database engine never interpolates user input into query strings. This is fundamentally safer than string concatenation approaches used in some ORMs.

For text fields that get stored and later displayed (like blog post content), add HTML sanitization to prevent XSS:

import "github.com/microcosm-cc/bluemonday"
 
var sanitizer = bluemonday.UGCPolicy()
 
func sanitizeInput(s string) string {
    return sanitizer.Sanitize(s)
}
 
// In your service layer:
func (s *PostService) CreatePost(ctx context.Context, params CreatePostParams) (db.Post, error) {
    return s.queries.CreatePost(ctx, db.CreatePostParams{
        UserID:  params.UserID,
        Title:   sanitizeInput(params.Title),
        Content: sanitizeInput(params.Content),
    })
}

Observability: Structured Logging, Metrics, and Tracing

Structured Logging with slog

Go 1.21 introduced log/slog as a structured logging standard library — no more third-party dependency required for basic needs.

// internal/middleware/logger.go
package middleware
 
import (
    "log/slog"
    "time"
 
    "github.com/labstack/echo/v4"
)
 
func StructuredLogger() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            start := time.Now()
            err := next(c)
            duration := time.Since(start)
 
            logAttrs := []any{
                "method",     c.Request().Method,
                "path",       c.Request().URL.Path,
                "status",     c.Response().Status,
                "duration_ms", duration.Milliseconds(),
                "request_id", c.Response().Header().Get(echo.HeaderXRequestID),
                "ip",         c.RealIP(),
            }
 
            if err != nil {
                logAttrs = append(logAttrs, "error", err.Error())
                slog.Error("Request failed", logAttrs...)
            } else if c.Response().Status >= 500 {
                slog.Error("Server error response", logAttrs...)
            } else {
                slog.Info("Request handled", logAttrs...)
            }
 
            return err
        }
    }
}

Prometheus Metrics Integration

// Add Prometheus metrics endpoint for Cloud Monitoring integration
import "github.com/prometheus/client_golang/prometheus/promhttp"
 
// In router.go
e.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
 
// Custom business metrics
var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path"},
    )
)
 
func init() {
    prometheus.MustRegister(requestsTotal, requestDuration)
}

OpenTelemetry Distributed Tracing

For microservice architectures where a single request spans multiple services, distributed tracing is essential for debugging latency issues. Ask Antigravity: "Add OpenTelemetry tracing to my Echo handlers that propagates trace context through PostgreSQL queries."

// Instrument DB queries with OpenTelemetry spans
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/trace"
)
 
func (h *PostHandler) List(c echo.Context) error {
    ctx := c.Request().Context()
    tracer := otel.Tracer("post-handler")
 
    ctx, span := tracer.Start(ctx, "post.list")
    defer span.End()
 
    posts, err := h.queries.ListPosts(ctx, db.ListPostsParams{
        UserID: getUserIDFromJWT(c),
        Limit:  20,
        Offset: 0,
    })
    if err != nil {
        span.RecordError(err)
        return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch posts")
    }
 
    return c.JSON(http.StatusOK, posts)
}

Advanced Patterns: Dependency Injection and Clean Architecture

Wire-Based Dependency Injection

As your application grows, manual dependency wiring in main.go becomes unwieldy. Google's Wire tool generates compile-time dependency injection code — and Antigravity can help you write the provider definitions:

// internal/wire.go (Wire provider definitions)
//go:build wireinject
 
package internal
 
import (
    "github.com/google/wire"
    "github.com/yourname/myapi/internal/api"
    "github.com/yourname/myapi/internal/db"
    "github.com/yourname/myapi/internal/service"
)
 
var appSet = wire.NewSet(
    db.New,
    service.NewUserService,
    service.NewPostService,
    api.NewUserHandler,
    api.NewPostHandler,
    api.NewRouter,
)
 
func InitializeApp(pool *pgxpool.Pool) (*echo.Echo, error) {
    wire.Build(appSet)
    return nil, nil
}
# Generate dependency injection code
wire ./internal/...
# → Generates wire_gen.go with all wiring resolved at compile time

Repository Pattern for Testable Services

Rather than calling sqlc-generated code directly from service functions, wrap queries in a repository interface. Antigravity can generate both the interface and a mock implementation for unit tests:

// internal/repository/user_repository.go
type UserRepository interface {
    Create(ctx context.Context, params CreateParams) (User, error)
    GetByEmail(ctx context.Context, email string) (User, error)
    GetByID(ctx context.Context, id uuid.UUID) (User, error)
    List(ctx context.Context, limit, offset int32) ([]User, error)
}
 
// internal/repository/mock_user_repository.go (generated by Antigravity or mockery)
type MockUserRepository struct {
    mock.Mock
}
 
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (User, error) {
    args := m.Called(ctx, email)
    return args.Get(0).(User), args.Error(1)
}

With this pattern, service-layer unit tests run in milliseconds without Docker, while integration tests use the real PostgreSQL via testcontainers. You get the best of both worlds: fast feedback during development and high confidence before deployment.


Common Errors and How to Fix Them

Error 1: pgx: cannot convert type unknown

This occurs when sqlc-generated code doesn't handle UUID types correctly.

# Ask Antigravity: "Fix the pgx UUID type handling issue"
# → Antigravity generates the correct pgtype.UUID conversion code

Error 2: context deadline exceeded — DB Connection Timeout

This typically means pgxpool's settings are too conservative, or you've hit the connection limit.

config, _ := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
config.MaxConns = 25           // Align with Cloud Run max-instances
config.MinConns = 5            // Keep warmed-up connections ready
config.MaxConnLifetime = 30 * time.Minute
config.MaxConnIdleTime = 5 * time.Minute

Error 3: exec format error with Distroless Image

This happens when you build locally on ARM (Apple Silicon) and deploy to AMD64 Cloud Run.

# Cross-platform build for AMD64
docker buildx build \
  --platform linux/amd64 \
  -t asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest \
  --push .

Summary

In this article, we walked through a complete workflow for building a production-grade RESTful API using Antigravity and Go. Let's recap the key takeaways:

  • A well-crafted AGENTS.md dramatically improves the quality of Antigravity's code generation
  • sqlc × pgx delivers type-safe, high-performance database access without the magic of an ORM
  • testcontainers-go integration tests catch production-level bugs that mocks would never surface
  • Distroless images × Cloud Run provide a secure, scalable production environment
  • Antigravity's Agent isn't just for boilerplate — it's a capable design partner, debugger, and optimization advisor

For further exploration, check out our Kubernetes Container Orchestration Practical Guide and Clean Architecture × DDD Implementation Guide to build even more robust backend systems.

The combination of Antigravity's AI intelligence and Go's performance characteristics creates a compelling stack for backend development. TypeScript gives you flexibility; Go gives you predictability and speed. Used together with an AI IDE that truly understands your codebase, Go stops feeling like a "harder" language and starts feeling like the right tool for systems that need to scale.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-03-31
Antigravity × gRPC & Protocol Buffers — A Practical Guide to High-Performance Microservice API Design
Learn to design and implement gRPC + Protocol Buffers microservice APIs using Antigravity's AI agents. Covers schema-driven development, streaming patterns, authentication, and error handling for production systems.
App Dev2026-06-19
Designing Safe Background Tasks with the Managed Agents API
Antigravity 2.0's Managed Agents API launches an agent in an isolated Linux environment with a single API call, handling reasoning, tool use, and code execution. Convenient, but left unattended it invites runaways and cost overruns. Here is a design for running it safely as a background task.
App Dev2026-05-14
CORS Errors Blocking API Tests in Antigravity — How to Diagnose and Fix by Environment
Fix CORS errors that block API testing in Antigravity development. Learn how to identify the root cause by environment — Next.js, Cloudflare Workers, Supabase — and craft precise prompts so Antigravity generates the right fix.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →