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:
- "What does the developer want to accomplish?"
- "What's the most intuitive way to express this?"
- "How do we make errors helpful instead of cryptic?"
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:
- Understand your API in 5 minutes
- Make their first successful call in 10 minutes
- Build something useful in 15 minutes
If it takes longer, your API UX needs work.
Tools That Help
- Swagger/OpenAPI: Generate docs from your Go structs
- Postman Collections: Make testing easy
- Go Playground examples: Let people experiment
- SDK generation: Reduce integration friction
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. ✨