Skip to content
Back to skills

Chi

ASecurity

Chi lightweight Go HTTP router. Covers routing, middleware, context, and patterns. Use for idiomatic, stdlib-compatible Go APIs. USE WHEN: user mentions "chi", "go-chi", "lightweight go router", "stdlib go router", asks about "chi middleware", "chi router", "chi context", "idiomatic go api", "net/http compatible router", "chi patterns" DO NOT USE FOR: Gin projects - use `gin` instead, Echo projects - use `echo` instead, Fiber projects - use `fiber` instead, non-Go backends

  • 31 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 8, 2026
ai-agentsgosqlexpressgitapidatabasebackenddocumentation

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill chi --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Chi?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Chi
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-chi/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-chi)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: chi
description: |
  Chi lightweight Go HTTP router. Covers routing, middleware, context,
  and patterns. Use for idiomatic, stdlib-compatible Go APIs.

  USE WHEN: user mentions "chi", "go-chi", "lightweight go router", "stdlib go router",
  asks about "chi middleware", "chi router", "chi context", "idiomatic go api",
  "net/http compatible router", "chi patterns"

  DO NOT USE FOR: Gin projects - use `gin` instead, Echo projects - use `echo` instead,
  Fiber projects - use `fiber` instead, non-Go backends
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Chi Core Knowledge

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `chi` for comprehensive documentation.

## Basic Setup

```go
package main

import (
    "net/http"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()

    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    http.ListenAndServe(":8080", r)
}
```

## Routing

### Basic Routes

```go
r := chi.NewRouter()

r.Get("/users", listUsers)
r.Get("/users/{id}", getUser)
r.Post("/users", createUser)
r.Put("/users/{id}", updateUser)
r.Delete("/users/{id}", deleteUser)

// Method not allowed handler
r.MethodNotAllowed(methodNotAllowedHandler)

// Not found handler
r.NotFound(notFoundHandler)
```

### Path Parameters

```go
r.Get("/users/{userID}", func(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    json.NewEncoder(w).Encode(map[string]string{"id": userID})
})

// Regex constraints
r.Get("/articles/{date:\\d{4}-\\d{2}-\\d{2}}", getArticleByDate)

// Catch-all
r.Get("/files/*", serveFiles)
```

### Route Groups

```go
r := chi.NewRouter()

r.Route("/api", func(r chi.Router) {
    r.Route("/v1", func(r chi.Router) {
        r.Get("/users", listUsersV1)
        r.Post("/users", createUserV1)
    })

    r.Route("/v2", func(r chi.Router) {
        r.Get("/users", listUsersV2)
        r.Post("/users", createUserV2)
    })
})
```

### Subrouters

```go
r := chi.NewRouter()

// Mount subrouter
apiRouter := chi.NewRouter()
apiRouter.Get("/users", listUsers)
apiRouter.Post("/users", createUser)

r.Mount("/api/v1", apiRouter)
```

## Context

### Request Context

```go
func getUser(w http.ResponseWriter, r *http.Request) {
    // Path params
    userID := chi.URLParam(r, "userID")

    // Query params
    page := r.URL.Query().Get("page")

    // Headers
    auth := r.Header.Get("Authorization")

    // Context values
    user := r.Context().Value("user").(*User)

    json.NewEncoder(w).Encode(user)
}
```

### Context with Values

```go
func UserContext(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := chi.URLParam(r, "userID")
        user, err := findUser(userID)
        if err != nil {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }

        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

r.Route("/users/{userID}", func(r chi.Router) {
    r.Use(UserContext)
    r.Get("/", getUser)
    r.Put("/", updateUser)
})
```

## Middleware

### Built-in Middleware

```go
import "github.com/go-chi/chi/v5/middleware"

r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Compress(5))
r.Use(middleware.Timeout(60 * time.Second))
r.Use(middleware.Throttle(100))
r.Use(middleware.Heartbeat("/ping"))
```

### Custom Middleware

```go
func TimingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        duration := time.Since(start)
        log.Printf("%s %s %v", r.Method, r.URL.Path, duration)
    })
}

r.Use(TimingMiddleware)
```

### Authentication Middleware

```go
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")

        if auth == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        if !strings.HasPrefix(auth, "Bearer ") {
            http.Error(w, "Invalid token format", http.StatusUnauthorized)
            return
        }

        token := auth[7:]
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

r.Route("/api", func(r chi.Router) {
    r.Use(AuthMiddleware)
    r.Get("/me", getMe)
})

func getMe(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value("user").(*User)
    json.NewEncoder(w).Encode(user)
}
```

### CORS Middleware

```go
import "github.com/go-chi/cors"

r.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"https://example.com"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type"},
    AllowCredentials: true,
    MaxAge:           300,
}))
```

## Response Helpers

### render Package

```go
import "github.com/go-chi/render"

type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (u *UserResponse) Render(w http.ResponseWriter, r *http.Request) error {
    return nil
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := &UserResponse{
        ID:    "1",
        Name:  "Alice",
        Email: "alice@example.com",
    }
    render.JSON(w, r, user)
}

// Error response
type ErrResponse struct {
    Err            error  `json:"-"`
    HTTPStatusCode int    `json:"-"`
    StatusText     string `json:"status"`
    ErrorText      string `json:"error,omitempty"`
}

func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
    render.Status(r, e.HTTPStatusCode)
    return nil
}

func ErrNotFound(err error) render.Renderer {
    return &ErrResponse{
        Err:            err,
        HTTPStatusCode: http.StatusNotFound,
        StatusText:     "Resource not found",
        ErrorText:      err.Error(),
    }
}
```

### Manual JSON Response

```go
func respond(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: "1", Name: "Alice"}
    respond(w, http.StatusOK, user)
}
```

## Error Handling

```go
type APIError struct {
    StatusCode int    `json:"-"`
    Message    string `json:"message"`
    Details    string `json:"details,omitempty"`
}

func (e *APIError) Error() string {
    return e.Message
}

func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        respond(w, apiErr.StatusCode, apiErr)
        return
    }

    respond(w, http.StatusInternalServerError, map[string]string{
        "message": "Internal server error",
    })
}

func getUser(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    user, err := findUser(userID)
    if err != nil {
        errorHandler(w, r, &APIError{
            StatusCode: http.StatusNotFound,
            Message:    "User not found",
        })
        return
    }
    respond(w, http.StatusOK, user)
}
```

## Patterns

### Resource Pattern

```go
type UsersResource struct {
    db *sql.DB
}

func (rs UsersResource) Routes() chi.Router {
    r := chi.NewRouter()

    r.Get("/", rs.List)
    r.Post("/", rs.Create)

    r.Route("/{id}", func(r chi.Router) {
        r.Use(rs.UserCtx)
        r.Get("/", rs.Get)
        r.Put("/", rs.Update)
        r.Delete("/", rs.Delete)
    })

    return r
}

func (rs UsersResource) UserCtx(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")
        user, err := rs.db.FindUser(id)
        if err != nil {
            http.Error(w, "User not found", http.StatusNotFound)
            return
        }
        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Mount resource
r.Mount("/users", UsersResource{db: db}.Routes())
```

## Production Readiness

### Health Checks

```go
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
    respond(w, http.StatusOK, map[string]string{"status": "healthy"})
})

r.Get("/ready", func(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    if err := db.PingContext(ctx); err != nil {
        respond(w, http.StatusServiceUnavailable, map[string]string{
            "status":   "not ready",
            "database": "disconnected",
        })
        return
    }

    respond(w, http.StatusOK, map[string]string{
        "status":   "ready",
        "database": "connected",
    })
})
```

### Graceful Shutdown

```go
func main() {
    r := chi.NewRouter()
    r.Get("/", handler)

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(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()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal(err)
    }
}
```

### Checklist

- [ ] Recoverer middleware enabled
- [ ] CORS configured
- [ ] Request timeout set
- [ ] Throttling enabled
- [ ] Health/readiness endpoints
- [ ] Graceful shutdown
- [ ] Structured logging
- [ ] Context-based values

## When NOT to Use This Skill

- **Gin projects** - Gin has more built-in features
- **Echo projects** - Echo has different context patterns
- **Fiber projects** - Fiber is Express-like and faster
- **Need built-in validation** - Chi is minimal, use Gin or Echo
- **WebSocket-heavy apps** - Other frameworks have better support

## Anti-Patterns

| Anti-Pattern | Why It's Bad | Solution |
|--------------|--------------|----------|
| Not using `chi.URLParam()` | Manual param extraction | Use `chi.URLParam(r, "id")` |
| Missing middleware order | Wrong execution sequence | Order middleware carefully |
| Not using context for values | Global state issues | Use `context.WithValue()` |
| Forgetting to call `next.ServeHTTP()` | Handler chain breaks | Always call `next.ServeHTTP(w, r)` |
| No structured error handling | Inconsistent responses | Create helper functions |
| Using subrouters incorrectly | Routes don't match | Use `.Route()` or `.Mount()` properly |

## Quick Troubleshooting

| Problem | Diagnosis | Fix |
|---------|-----------|-----|
| Route not matching | Wrong path pattern | Check regex constraints and params |
| Middleware not executing | Order issue | Place middleware before routes |
| Context value is nil | Not set properly | Use `context.WithValue()` in middleware |
| CORS errors | Not configured | Use `cors.Handler()` from go-chi/cors |
| Panic on missing param | No default | Check `chi.URLParam()` return value |
| 405 Method Not Allowed | Wrong HTTP method | Verify route method matches request |

## Reference Documentation

- [Routing](quick-ref/routing.md)
- [Middleware](quick-ref/middleware.md)
- [Patterns](quick-ref/patterns.md)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.