| name | backend-reference |
| description | Complete backend best practices reference covering Go, Python, SQL, and API design. Includes error handling, concurrency, query optimization, REST conventions, and common anti-patterns with before/after examples. Load this when you need to verify a backend pattern during code review.
|
Backend Reference — Go, Python, SQL & API Design Best Practices
Go Patterns
Error Handling
user, err := db.FindUser(ctx, id)
if err != nil {
return fmt.Errorf("finding user %s: %w", id, err)
}
user, err := db.FindUser(ctx, id)
if err != nil {
return err
}
user, _ := db.FindUser(ctx, id)
var ErrNotFound = errors.New("not found")
var ErrConflict = errors.New("conflict")
if errors.Is(err, ErrNotFound) {
return http.StatusNotFound
}
if err.Error() == "not found" {
Concurrency
func (s *Service) ProcessItems(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
return s.processOne(ctx, item)
})
}
return g.Wait()
}
func (s *Service) ProcessItems(items []Item) {
for _, item := range items {
go s.processOne(item)
}
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user, err := h.service.FindUser(ctx, chi.URLParam(r, "id"))
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.FindUser(context.Background(), id)
}
Interface Design
type UserReader interface {
FindUser(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
CreateUser(ctx context.Context, user *User) error
}
func NewUserService(repo UserReader) *UserService {
return &UserService{repo: repo}
}
type UserRepository interface {
FindUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context, filter Filter) ([]*User, error)
CountUsers(ctx context.Context) (int, error)
}
Naming Conventions
type HTTPClient struct{}
func (u *User) ID() string {}
var userID string
var ErrNotFound error
type HttpClient struct{}
func (u *User) GetID() string {}
var userId string
Testing
func TestParseAmount(t *testing.T) {
tests := []struct {
name string
input string
want int64
wantErr bool
}{
{name: "valid integer", input: "100", want: 100},
{name: "valid decimal", input: "10.50", want: 1050},
{name: "negative", input: "-5", want: 0, wantErr: true},
{name: "empty string", input: "", want: 0, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseAmount(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParseAmount(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseAmount(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
Struct Tags
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
type CreateUserRequest struct {
Name string
Email string `json:"email"`
age int
}
Python Patterns
Type Hints
from typing import Optional, Sequence
def find_users(
filters: dict[str, str],
limit: int = 50,
offset: int = 0,
) -> Sequence[User]:
...
def find_users(filters, limit=50, offset=0):
...
from typing import Any
def process(data: Any) -> Any:
...
Error Handling
class UserNotFoundError(DomainError):
def __init__(self, user_id: str) -> None:
super().__init__(f"User {user_id} not found")
self.user_id = user_id
try:
user = await repo.find(user_id)
except DatabaseError as e:
raise UserNotFoundError(user_id) from e
try:
user = await repo.find(user_id)
except:
pass
try:
user = await repo.find(user_id)
except Exception:
return None
Mutable Default Arguments
def add_item(item: str, items: list[str] = []) -> list[str]:
items.append(item)
return items
def add_item(item: str, items: list[str] | None = None) -> list[str]:
if items is None:
items = []
items.append(item)
return items
FastAPI Patterns
@router.post("/users", status_code=201, response_model=UserResponse)
async def create_user(
request: CreateUserRequest,
service: UserService = Depends(get_user_service),
) -> UserResponse:
user = await service.create(request)
return UserResponse.from_domain(user)
@router.post("/users")
async def create_user(request: Request):
body = await request.json()
if not body.get("name"):
raise HTTPException(400, "name required")
db = get_database()
...
Async Patterns
async def get_dashboard(user_id: str) -> Dashboard:
user, orders, notifications = await asyncio.gather(
user_service.find(user_id),
order_service.list(user_id),
notification_service.unread(user_id),
)
return Dashboard(user=user, orders=orders, notifications=notifications)
async def get_dashboard(user_id: str) -> Dashboard:
user = await user_service.find(user_id)
orders = await order_service.list(user_id)
notifications = await notification_service.unread(user_id)
SQL & Database Patterns
Parameterized Queries
db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", userID)
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
db.QueryRow(ctx, fmt.Sprintf("SELECT * FROM users WHERE id = '%s'", userID))
cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'")
N+1 Query Detection
orders = Order.objects.all()
for order in orders:
print(order.user.name)
orders = Order.objects.select_related("user").all()
for order in orders:
print(order.user.name)
orders, _ := repo.ListOrders(ctx)
for _, order := range orders {
user, _ := repo.FindUser(ctx, order.UserID)
}
orders, _ := repo.ListOrders(ctx)
userIDs := extractUserIDs(orders)
users, _ := repo.FindUsersByIDs(ctx, userIDs)
userMap := indexByID(users)
Index Strategy
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_users_active ON users(is_active);
CREATE INDEX idx_users_active ON users(id) WHERE is_active = true;
Migration Safety
ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
ALTER TABLE users ADD COLUMN status VARCHAR(20) NOT NULL;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
Pagination
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 10000;
SELECT * FROM orders
WHERE created_at < '2024-01-15T10:30:00Z'
ORDER BY created_at DESC
LIMIT 20;
API Design Patterns
REST Resource Naming
GOOD:
GET /users — list users
POST /users — create user
GET /users/{id} — get user
PUT /users/{id} — replace user
PATCH /users/{id} — partial update
DELETE /users/{id} — delete user
GET /users/{id}/orders — list user's orders
BAD:
GET /getUsers — verb in URL
POST /createUser — verb in URL
GET /user/list — singular + verb
DELETE /users/{id}/delete — redundant verb
Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be between 0 and 150" }
]
}
}
{ "error": "pq: duplicate key value violates unique constraint \"users_email_key\"" }
Status Code Quick Reference
| Code | When | Notes |
|---|
| 200 | Successful GET, PUT, PATCH | Include body |
| 201 | Successful POST (created) | Include Location header |
| 204 | Successful DELETE | No body |
| 400 | Malformed request | Can't parse JSON, wrong Content-Type |
| 401 | Not authenticated | Missing or invalid token |
| 403 | Not authorized | Valid token, insufficient permissions |
| 404 | Resource not found | Also use for unauthorized resource access (prevent enumeration) |
| 409 | Conflict | Duplicate resource, version conflict |
| 422 | Validation error | Valid JSON but invalid field values |
| 429 | Rate limited | Include Retry-After header |
| 500 | Server error | Never expose internals |
Pagination Response
{
"data": [...],
"pagination": {
"total": 150,
"page": 2,
"pageSize": 20,
"hasMore": true,
"nextCursor": "eyJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNSJ9"
}
}
Idempotency
// Client sends idempotency key with POST
POST /payments
Idempotency-Key: abc-123
Content-Type: application/json
{ "amount": 1000, "currency": "USD" }
// Server behavior:
// 1st request: process payment, store result keyed by "abc-123"
// 2nd request with same key: return stored result, don't process again
// Different key: process as new payment
Common Anti-Patterns
| Anti-Pattern | Language | Why It's Bad | Fix |
|---|
Bare return err | Go | No context for debugging | fmt.Errorf("doing X: %w", err) |
| Fire-and-forget goroutine | Go | Leak, no error handling | errgroup, WaitGroup |
except: pass | Python | Swallowed errors | Specific exception + logging |
| Mutable default arg | Python | Shared state across calls | None sentinel |
SELECT * | SQL | Wastes bandwidth, breaks on schema change | Explicit columns |
| String interpolation in SQL | All | SQL injection | Parameterized queries |
| N+1 queries | All | O(n) queries instead of O(1) | Batch load, JOINs |
| OFFSET pagination | SQL | Slow for large offsets | Cursor/keyset pagination |
| Verb in REST URL | API | Not RESTful | Noun-based resources |
| 200 for errors | API | Breaks client error handling | Proper status codes |
| Stack trace in error response | API | Info leakage | Generic user message |