← Back to Blog

Building Go APIs That Don't Suck: A Designer's Guide

Lessons learned from designing and building Go APIs at Google that prioritize developer experience and human usability.

Hot take: Most APIs are designed by engineers for engineers, which explains why they're often terrible UX.

As a UX designer who codes Go at Google, I've learned that APIs are user interfaces too. The users just happen to be developers instead of end customers.

API Design is UX Design 🎨

The Mindset Shift

Instead of thinking "What can this system do?" ask:

Go's API Design Superpowers

1. Types as Documentation

// Bad: Vague and confusing
func CreateUser(data map[string]interface{}) error

// Good: Self-documenting
type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Name     string `json:"name" validate:"required,min=2"`
    Role     Role   `json:"role" validate:"required"`
}

func CreateUser(req CreateUserRequest) (*User, error)

The type tells a story. What's required? What format? What are the constraints? All clear from the signature.

2. Errors That Actually Help

// Bad: Panic-inducing
return fmt.Errorf("invalid input")

// Good: Actionable guidance
return fmt.Errorf("email '%s' is invalid: must be a valid email address (example: user@domain.com)", email)

// Even better: Structured errors
type ValidationError struct {
    Field   string `json:"field"`
    Value   string `json:"value"`
    Message string `json:"message"`
    Code    string `json:"code"`
}

Error messages are microcopy. Make them helpful, not hostile.

3. Consistent Response Patterns

// Standard error response
type ErrorResponse struct {
    Error   string            `json:"error"`
    Code    string            `json:"code"`
    Details map[string]string `json:"details,omitempty"`
}

4. Thoughtful Defaults

type UploadConfig struct {
    MaxSize     int64             `json:"max_size,omitempty"`
    AllowedTypes []string         `json:"allowed_types,omitempty"`
    Compress    bool              `json:"compress"`
    Metadata    map[string]string `json:"metadata,omitempty"`
}

type UploadResponse struct {
    FileID      string            `json:"file_id"`
    URL         string            `json:"url"`
    Size        int64             `json:"size"`
    ContentType string            `json:"content_type"`
}

Real-World API Patterns That Work

Request/Response Wrapping

// Wrap everything in a consistent structure
type CreateUserRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

Standard Response Envelope

type APIResponse[T any] struct {
    Success   bool              `json:"success"`
    Data      T                 `json:"data"`
    Error     *ErrorResponse    `json:"error,omitempty"`
    Meta      map[string]any    `json:"meta,omitempty"`
    RequestID string            `json:"request_id"`
    Timestamp time.Time         `json:"timestamp"`
}

Pagination That Doesn't Suck

type PaginationParams struct {
    Page     int    `json:"page" validate:"min=1"`
    Limit    int    `json:"limit" validate:"min=1,max=100"`
    SortBy   string `json:"sort_by,omitempty"`
    SortDir  string `json:"sort_dir,omitempty" validate:"oneof=asc desc"`
    Search   string `json:"search,omitempty"`
}

Include Everything Developers Need:

type UserListResponse struct {
    Users      []User `json:"users"`
    Pagination struct {
        CurrentPage  int  `json:"current_page"`
        TotalPages   int  `json:"total_pages"`
        TotalItems   int  `json:"total_items"`
        ItemsPerPage int  `json:"items_per_page"`
        HasNext      bool `json:"has_next"`
        HasPrev      bool `json:"has_prev"`
    } `json:"pagination"`
}

Documentation as Code

Your Go structs ARE your API documentation when done right:

type User struct {
    ID       string    `json:"id" example:"usr_123456789"`
    Email    string    `json:"email" example:"user@example.com"`
    Name     string    `json:"name" example:"Jane Smith"`
    Role     string    `json:"role" example:"admin" enums:"admin,user,viewer"`
    Created  time.Time `json:"created_at"`
    LastSeen time.Time `json:"last_seen_at"`
}

Error Handling Philosophy

// Production-ready error handling
func (s *UserService) GetUser(id string) (*User, error) {
    if id == "" {
        return nil, &ValidationError{
            Field:   "id",
            Message: "User ID is required",
            Code:    "MISSING_USER_ID",
        }
    }
    
    user, err := s.repo.GetByID(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, &NotFoundError{
                Resource: "user",
                ID:       id,
                Code:     "USER_NOT_FOUND",
            }
        }
        return nil, fmt.Errorf("failed to get user: %w", err)
    }
    
    return user, nil
}

The 15-Minute Rule

A new developer should be able to:

If it takes longer, your API UX needs work.

Tools That Help

The Kawaii Touch

Even APIs can have personality! Your error messages don't have to be cold and robotic:

"Oops! That email address looks a bit funky. Mind double-checking it? We're expecting something like user@domain.com 🌸"

Just remember: helpful first, personality second.

Testing Your API UX

Give your API to someone who's never seen it before. Watch them try to use it. The places they get confused? Those are your UX problems.

Build APIs that feel intuitive, not just functional. Your future self (and your teammates) will thank you. ✨