snapshot of https://github.com/go-chi/chi.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Test / test (1.24.x, ubuntu-latest) (push) Successful in 2m56s
Test / test (1.25.x, ubuntu-latest) (push) Successful in 2m15s
Test / test (1.26.x, ubuntu-latest) (push) Successful in 2m23s
Test / test (1.27.x, ubuntu-latest) (push) Failing after 3m56s
Test / test (1.24.x, windows-latest) (push) Has been cancelled
Test / test (1.25.x, windows-latest) (push) Has been cancelled
Test / test (1.26.x, windows-latest) (push) Has been cancelled
Test / test (1.27.x, windows-latest) (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 21:48:17 +05:00
commit 72c9f2ee75
105 changed files with 14011 additions and 0 deletions

25
_examples/README.md Normal file
View File

@@ -0,0 +1,25 @@
chi examples
============
* [custom-handler](https://github.com/go-chi/chi/blob/master/_examples/custom-handler/main.go) - Use a custom handler function signature
* [custom-method](https://github.com/go-chi/chi/blob/master/_examples/custom-method/main.go) - Add a custom HTTP method
* [fileserver](https://github.com/go-chi/chi/blob/master/_examples/fileserver/main.go) - Easily serve static files
* [graceful](https://github.com/go-chi/chi/blob/master/_examples/graceful/main.go) - Graceful context signaling and server shutdown
* [hello-world](https://github.com/go-chi/chi/blob/master/_examples/hello-world/main.go) - Hello World!
* [limits](https://github.com/go-chi/chi/blob/master/_examples/limits/main.go) - Timeouts and Throttling
* [logging](https://github.com/go-chi/chi/blob/master/_examples/logging/main.go) - Easy structured logging for any backend
* [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) - REST APIs made easy, productive and maintainable
* [router-walk](https://github.com/go-chi/chi/blob/master/_examples/router-walk/main.go) - Print to stdout a router's routes
* [todos-resource](https://github.com/go-chi/chi/blob/master/_examples/todos-resource/main.go) - Struct routers/handlers, an example of another code layout style
* [versions](https://github.com/go-chi/chi/blob/master/_examples/versions/main.go) - Demo of `chi/render` subpkg
* [pathvalue](https://github.com/go-chi/chi/blob/master/_examples/pathvalue/main.go) - Demonstrates `PathValue` usage for retrieving URL parameters
## Usage
1. `go get -v -d -u ./...` - fetch example deps
2. `cd <example>/` ie. `cd rest/`
3. `go run *.go` - note, example services run on port 3333
4. Open another terminal and use curl to send some requests to your example service,
`curl -v http://localhost:3333/`
5. Read <example>/main.go source to learn how service works and read comments for usage

6
_examples/chi.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.4835 183.994C49.265 184.012 43.1375 181.976 38.1326 179.467C25.868 173.318 19.7818 162.507 19.7818 147.233C19.7818 131.964 25.8983 121.219 38.1326 115.004C43.1705 112.56 49.2857 111.048 56.4835 110.475H72.8808C76.4783 110.475 79.2862 111.984 81.3002 115.004C83.3143 118.021 84.3213 121.542 84.3213 125.567C84.3213 129.592 83.3143 133.114 81.3002 136.132C79.2862 139.149 76.4783 140.659 72.8808 140.659H56.4835C52.1637 140.659 50.0065 142.852 50.0065 147.233C50.0065 151.619 52.1637 153.824 56.4835 153.812L114.737 153.538C118.335 153.529 121.143 155.05 123.157 158.067C125.168 161.084 126.179 164.605 126.179 168.63C126.179 172.655 125.171 176.177 123.157 179.195C121.143 182.212 118.333 183.714 114.737 183.722L56.4835 183.994Z" fill="#01933F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M206.93 172.348V120.747C206.93 117.154 208.442 114.388 211.463 112.377C214.487 110.361 218.013 109.357 222.042 109.357C226.071 109.357 229.597 110.361 232.622 112.377C235.643 114.388 237.155 117.154 237.155 120.747V172.348C237.155 175.941 235.643 178.709 232.622 180.721C229.597 182.732 226.071 183.739 222.042 183.739C218.013 183.739 214.487 182.732 211.463 180.721C208.442 178.709 206.93 175.941 206.93 172.348Z" fill="#01933F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M222.042 101.164C217.869 101.164 214.307 99.6901 211.356 96.7444C208.406 93.7973 206.93 90.2402 206.93 86.0717C206.93 81.9032 208.406 78.3461 211.356 75.399C214.307 72.4533 217.869 70.9818 222.042 70.9818C226.216 70.9818 229.778 72.4533 232.728 75.399C235.679 78.3461 237.155 81.9032 237.155 86.0717C237.155 90.2402 235.679 93.7973 232.728 96.7444C229.778 99.6901 226.216 101.164 222.042 101.164Z" fill="#83C21F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M163.754 172.312V146.441C163.754 142.13 161.594 139.972 157.277 139.972H135.047C119.793 139.972 109.068 133.864 102.88 121.646C100.434 116.617 98.9216 110.507 98.3452 103.321V86.0717C98.3452 82.4789 99.8585 79.6747 102.88 77.6632C105.903 75.6518 109.429 74.6461 113.459 74.6461C117.487 74.6461 121.013 75.6518 124.037 77.6632C127.059 79.6747 128.571 82.4789 128.571 86.0717V103.321C128.571 107.633 130.728 109.789 135.047 109.789H157.277C164.474 110.361 170.59 111.873 175.628 114.317C187.862 120.497 193.979 131.205 193.979 146.441V172.312C193.979 175.906 192.467 178.709 189.446 180.72C186.422 182.732 182.896 183.739 178.867 183.739C174.836 183.739 171.31 182.732 168.287 180.72C165.266 178.709 163.754 175.906 163.754 172.312Z" fill="#01933F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,35 @@
package main
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
)
type Handler func(w http.ResponseWriter, r *http.Request) error
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h(w, r); err != nil {
// handle returned error here.
w.WriteHeader(503)
w.Write([]byte("bad"))
}
}
func main() {
r := chi.NewRouter()
r.Method("GET", "/", Handler(customHandler))
http.ListenAndServe(":3333", r)
}
func customHandler(w http.ResponseWriter, r *http.Request) error {
q := r.URL.Query().Get("err")
if q != "" {
return errors.New(q)
}
w.Write([]byte("foo"))
return nil
}

View File

@@ -0,0 +1,33 @@
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func init() {
chi.RegisterMethod("LINK")
chi.RegisterMethod("UNLINK")
chi.RegisterMethod("WOOHOO")
}
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
r.MethodFunc("LINK", "/link", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("custom link method"))
})
r.MethodFunc("WOOHOO", "/woo", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("custom woohoo method"))
})
r.HandleFunc("/everything", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("capturing all standard http methods, as well as LINK, UNLINK and WOOHOO"))
})
http.ListenAndServe(":3333", r)
}

View File

@@ -0,0 +1 @@
Notessszzz

View File

@@ -0,0 +1,65 @@
// This example demonstrates how to serve static files from your filesystem.
//
// Boot the server:
//
// $ go run main.go
//
// Client requests:
//
// $ curl http://localhost:3333/files/
// <pre>
// <a href="notes.txt">notes.txt</a>
// </pre>
//
// $ curl http://localhost:3333/files/notes.txt
// Notessszzz
package main
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
// Index handler
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
// Create a route along /files that will serve contents from
// the ./data/ folder.
workDir, _ := os.Getwd()
filesDir := http.Dir(filepath.Join(workDir, "data"))
FileServer(r, "/files", filesDir)
http.ListenAndServe(":3333", r)
}
// FileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit any URL parameters.")
}
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,69 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
// The HTTP Server
server := &http.Server{Addr: "0.0.0.0:3333", Handler: service()}
// Create context that listens for the interrupt signal
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Run server in the background
go func() {
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
// Listen for the interrupt signal
<-ctx.Done()
// Create shutdown context with 30-second timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Trigger graceful shutdown
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatal(err)
}
}
func service() http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("sup"))
})
r.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
// Simulates some hard work.
//
// We want this handler to complete successfully during a shutdown signal,
// so consider the work here as some background routine to fetch a long-running
// search query to find as many results as possible, but, instead we cut it short
// and respond with what we have so far. How a shutdown is handled is entirely
// up to the developer, as some code blocks are preemptible, and others are not.
time.Sleep(5 * time.Second)
w.Write([]byte(fmt.Sprintf("all done.\n")))
})
return r
}

View File

@@ -0,0 +1,21 @@
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.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
})
http.ListenAndServe(":3333", r)
}

92
_examples/limits/main.go Normal file
View File

@@ -0,0 +1,92 @@
// This example demonstrates the use of Timeout, and Throttle middlewares.
//
// Timeout: cancel a request if processing takes longer than 2.5 seconds,
// server will respond with a http.StatusGatewayTimeout.
//
// Throttle: limit the number of in-flight requests along a particular
// routing path and backlog the others.
package main
import (
"context"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("test")
})
// Slow handlers/operations.
r.Group(func(r chi.Router) {
// Stop processing after 2.5 seconds.
r.Use(middleware.Timeout(2500 * time.Millisecond))
r.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
// Processing will take 1-5 seconds.
processTime := time.Duration(rand.Intn(4)+1) * time.Second
select {
case <-r.Context().Done():
return
case <-time.After(processTime):
// The above channel simulates some hard work.
}
w.Write([]byte(fmt.Sprintf("Processed in %v seconds\n", processTime)))
})
})
// Throttle very expensive handlers/operations.
r.Group(func(r chi.Router) {
// Stop processing after 30 seconds.
r.Use(middleware.Timeout(30 * time.Second))
// Only one request will be processed at a time.
r.Use(middleware.Throttle(1))
r.Get("/throttled", func(w http.ResponseWriter, r *http.Request) {
select {
case <-r.Context().Done():
switch r.Context().Err() {
case context.DeadlineExceeded:
w.WriteHeader(504)
w.Write([]byte("Processing too slow\n"))
default:
w.Write([]byte("Canceled\n"))
}
return
case <-time.After(5 * time.Second):
// The above channel simulates some hard work.
}
w.Write([]byte("Processed\n"))
})
})
http.ListenAndServe(":3333", r)
}

View File

@@ -0,0 +1,9 @@
package main
// Please see https://github.com/go-chi/httplog for a complete package
// and example for writing a structured logger on chi built on
// the Go 1.21+ "log/slog" package.
func main() {
// See https://github.com/go-chi/httplog/blob/master/_example/main.go
}

View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Registering a handler that retrieves a path parameter using PathValue
r.Get("/users/{userID}", pathValueHandler)
http.ListenAndServe(":3333", r)
}
// pathValueHandler retrieves a URL parameter using PathValue and writes it to the response.
func pathValueHandler(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("userID")
// Respond with the extracted userID
w.Write([]byte(fmt.Sprintf("User ID: %s", userID)))
}

9
_examples/rest/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module rest-example
go 1.16
require (
github.com/go-chi/chi/v5 v5.0.1
github.com/go-chi/docgen v1.2.0
github.com/go-chi/render v1.0.1
)

8
_examples/rest/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/go-chi/chi/v5 v5.0.1 h1:ALxjCrTf1aflOlkhMnCUP86MubbWFrzB3gkRPReLpTo=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/docgen v1.2.0 h1:da0Nq2PKU9W9pSOTUfVrKI1vIgTGpauo9cfh4Iwivek=
github.com/go-chi/docgen v1.2.0/go.mod h1:G9W0G551cs2BFMSn/cnGwX+JBHEloAgo17MBhyrnhPI=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

520
_examples/rest/main.go Normal file
View File

@@ -0,0 +1,520 @@
// This example demonstrates a HTTP REST web service with some fixture data.
// Follow along the example and patterns.
//
// Also check routes.json for the generated docs from passing the -routes flag,
// to run yourself do: `go run . -routes`
//
// Boot the server:
//
// $ go run main.go
//
// Client requests:
//
// $ curl http://localhost:3333/
// root.
//
// $ curl http://localhost:3333/articles
// [{"id":"1","title":"Hi"},{"id":"2","title":"sup"}]
//
// $ curl http://localhost:3333/articles/1
// {"id":"1","title":"Hi"}
//
// $ curl -X DELETE http://localhost:3333/articles/1
// {"id":"1","title":"Hi"}
//
// $ curl http://localhost:3333/articles/1
// "Not Found"
//
// $ curl -X POST -d '{"id":"will-be-omitted","title":"awesomeness"}' http://localhost:3333/articles
// {"id":"97","title":"awesomeness"}
//
// $ curl http://localhost:3333/articles/97
// {"id":"97","title":"awesomeness"}
//
// $ curl http://localhost:3333/articles
// [{"id":"2","title":"sup"},{"id":"97","title":"awesomeness"}]
package main
import (
"context"
"errors"
"flag"
"fmt"
"math/rand"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/docgen"
"github.com/go-chi/render"
)
var routes = flag.Bool("routes", false, "Generate router documentation")
func main() {
flag.Parse()
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("test")
})
// RESTy routes for "articles" resource
r.Route("/articles", func(r chi.Router) {
r.With(paginate).Get("/", ListArticles)
r.Post("/", CreateArticle) // POST /articles
r.Get("/search", SearchArticles) // GET /articles/search
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx) // Load the *Article on the request context
r.Get("/", GetArticle) // GET /articles/123
r.Put("/", UpdateArticle) // PUT /articles/123
r.Delete("/", DeleteArticle) // DELETE /articles/123
})
// GET /articles/whats-up
r.With(ArticleCtx).Get("/{articleSlug:[a-z-]+}", GetArticle)
})
// Mount the admin sub-router, which btw is the same as:
// r.Route("/admin", func(r chi.Router) { admin routes here })
r.Mount("/admin", adminRouter())
// Passing -routes to the program will generate docs for the above
// router definition. See the `routes.json` file in this folder for
// the output.
if *routes {
// fmt.Println(docgen.JSONRoutesDoc(r))
fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{
ProjectPath: "github.com/go-chi/chi/v5",
Intro: "Welcome to the chi/_examples/rest generated docs.",
}))
return
}
http.ListenAndServe(":3333", r)
}
func ListArticles(w http.ResponseWriter, r *http.Request) {
if err := render.RenderList(w, r, NewArticleListResponse(articles)); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}
// ArticleCtx middleware is used to load an Article object from
// the URL parameters passed through as the request. In case
// the Article could not be found, we stop here and return a 404.
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var article *Article
var err error
if articleID := chi.URLParam(r, "articleID"); articleID != "" {
article, err = dbGetArticle(articleID)
} else if articleSlug := chi.URLParam(r, "articleSlug"); articleSlug != "" {
article, err = dbGetArticleBySlug(articleSlug)
} else {
render.Render(w, r, ErrNotFound)
return
}
if err != nil {
render.Render(w, r, ErrNotFound)
return
}
ctx := context.WithValue(r.Context(), "article", article)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// SearchArticles searches the Articles data for a matching article.
// It's just a stub, but you get the idea.
func SearchArticles(w http.ResponseWriter, r *http.Request) {
render.RenderList(w, r, NewArticleListResponse(articles))
}
// CreateArticle persists the posted Article and returns it
// back to the client as an acknowledgement.
func CreateArticle(w http.ResponseWriter, r *http.Request) {
data := &ArticleRequest{}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
article := data.Article
dbNewArticle(article)
render.Status(r, http.StatusCreated)
render.Render(w, r, NewArticleResponse(article))
}
// GetArticle returns the specific Article. You'll notice it just
// fetches the Article right off the context, as its understood that
// if we made it this far, the Article must be on the context. In case
// its not due to a bug, then it will panic, and our Recoverer will save us.
func GetArticle(w http.ResponseWriter, r *http.Request) {
// Assume if we've reach this far, we can access the article
// context because this handler is a child of the ArticleCtx
// middleware. The worst case, the recoverer middleware will save us.
article := r.Context().Value("article").(*Article)
if err := render.Render(w, r, NewArticleResponse(article)); err != nil {
render.Render(w, r, ErrRender(err))
return
}
}
// UpdateArticle updates an existing Article in our persistent store.
func UpdateArticle(w http.ResponseWriter, r *http.Request) {
article := r.Context().Value("article").(*Article)
data := &ArticleRequest{Article: article}
if err := render.Bind(r, data); err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
article = data.Article
dbUpdateArticle(article.ID, article)
render.Render(w, r, NewArticleResponse(article))
}
// DeleteArticle removes an existing Article from our persistent store.
func DeleteArticle(w http.ResponseWriter, r *http.Request) {
var err error
// Assume if we've reach this far, we can access the article
// context because this handler is a child of the ArticleCtx
// middleware. The worst case, the recoverer middleware will save us.
article := r.Context().Value("article").(*Article)
article, err = dbRemoveArticle(article.ID)
if err != nil {
render.Render(w, r, ErrInvalidRequest(err))
return
}
render.Render(w, r, NewArticleResponse(article))
}
// A completely separate router for administrator routes
func adminRouter() chi.Router {
r := chi.NewRouter()
r.Use(AdminOnly)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin: index"))
})
r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin: list accounts.."))
})
r.Get("/users/{userId}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(r, "userId"))))
})
return r
}
// AdminOnly middleware restricts access to just administrators.
func AdminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
isAdmin, ok := r.Context().Value("acl.admin").(bool)
if !ok || !isAdmin {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
// paginate is a stub, but very possible to implement middleware logic
// to handle the request params for handling a paginated request.
func paginate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// just a stub.. some ideas are to look at URL query params for something like
// the page number, or the limit, and send a query cursor down the chain
next.ServeHTTP(w, r)
})
}
// This is entirely optional, but I wanted to demonstrate how you could easily
// add your own logic to the render.Respond method.
func init() {
render.Respond = func(w http.ResponseWriter, r *http.Request, v interface{}) {
if err, ok := v.(error); ok {
// We set a default error status response code if one hasn't been set.
if _, ok := r.Context().Value(render.StatusCtxKey).(int); !ok {
w.WriteHeader(400)
}
// We log the error
fmt.Printf("Logging err: %s\n", err.Error())
// We change the response to not reveal the actual error message,
// instead we can transform the message something more friendly or mapped
// to some code / language, etc.
render.DefaultResponder(w, r, render.M{"status": "error"})
return
}
render.DefaultResponder(w, r, v)
}
}
//--
// Request and Response payloads for the REST api.
//
// The payloads embed the data model objects an
//
// In a real-world project, it would make sense to put these payloads
// in another file, or another sub-package.
//--
type UserPayload struct {
*User
Role string `json:"role"`
}
func NewUserPayloadResponse(user *User) *UserPayload {
return &UserPayload{User: user}
}
// Bind on UserPayload will run after the unmarshalling is complete, its
// a good time to focus some post-processing after a decoding.
func (u *UserPayload) Bind(r *http.Request) error {
return nil
}
func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) error {
u.Role = "collaborator"
return nil
}
// ArticleRequest is the request payload for Article data model.
//
// NOTE: It's good practice to have well defined request and response payloads
// so you can manage the specific inputs and outputs for clients, and also gives
// you the opportunity to transform data on input or output, for example
// on request, we'd like to protect certain fields and on output perhaps
// we'd like to include a computed field based on other values that aren't
// in the data model. Also, check out this awesome blog post on struct composition:
// http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/
type ArticleRequest struct {
*Article
User *UserPayload `json:"user,omitempty"`
ProtectedID string `json:"id"` // override 'id' json to have more control
}
func (a *ArticleRequest) Bind(r *http.Request) error {
// a.Article is nil if no Article fields are sent in the request. Return an
// error to avoid a nil pointer dereference.
if a.Article == nil {
return errors.New("missing required Article fields.")
}
// a.User is nil if no Userpayload fields are sent in the request. In this app
// this won't cause a panic, but checks in this Bind method may be required if
// a.User or further nested fields like a.User.Name are accessed elsewhere.
// just a post-process after a decode..
a.ProtectedID = "" // unset the protected ID
a.Article.Title = strings.ToLower(a.Article.Title) // as an example, we down-case
return nil
}
// ArticleResponse is the response payload for the Article data model.
// See NOTE above in ArticleRequest as well.
//
// In the ArticleResponse object, first a Render() is called on itself,
// then the next field, and so on, all the way down the tree.
// Render is called in top-down order, like a http handler middleware chain.
type ArticleResponse struct {
*Article
User *UserPayload `json:"user,omitempty"`
// We add an additional field to the response here.. such as this
// elapsed computed property
Elapsed int64 `json:"elapsed"`
}
func NewArticleResponse(article *Article) *ArticleResponse {
resp := &ArticleResponse{Article: article}
if resp.User == nil {
if user, _ := dbGetUser(resp.UserID); user != nil {
resp.User = NewUserPayloadResponse(user)
}
}
return resp
}
func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Request) error {
// Pre-processing before a response is marshalled and sent across the wire
rd.Elapsed = 10
return nil
}
func NewArticleListResponse(articles []*Article) []render.Renderer {
list := []render.Renderer{}
for _, article := range articles {
list = append(list, NewArticleResponse(article))
}
return list
}
// NOTE: as a thought, the request and response payloads for an Article could be the
// same payload type, perhaps will do an example with it as well.
// type ArticlePayload struct {
// *Article
// }
//--
// Error response payloads & renderers
//--
// ErrResponse renderer type for handling all sorts of errors.
//
// In the best case scenario, the excellent github.com/pkg/errors package
// helps reveal information on the error, setting it on Err, and in the Render()
// method, using it to set the application-specific error code in AppCode.
type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
func ErrInvalidRequest(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 400,
StatusText: "Invalid request.",
ErrorText: err.Error(),
}
}
func ErrRender(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 422,
StatusText: "Error rendering response.",
ErrorText: err.Error(),
}
}
var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
//--
// Data model objects and persistence mocks:
//--
// User data model
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// Article data model. I suggest looking at https://upper.io for an easy
// and powerful data persistence adapter.
type Article struct {
ID string `json:"id"`
UserID int64 `json:"user_id"` // the author
Title string `json:"title"`
Slug string `json:"slug"`
}
// Article fixture data
var articles = []*Article{
{ID: "1", UserID: 100, Title: "Hi", Slug: "hi"},
{ID: "2", UserID: 200, Title: "sup", Slug: "sup"},
{ID: "3", UserID: 300, Title: "alo", Slug: "alo"},
{ID: "4", UserID: 400, Title: "bonjour", Slug: "bonjour"},
{ID: "5", UserID: 500, Title: "whats up", Slug: "whats-up"},
}
// User fixture data
var users = []*User{
{ID: 100, Name: "Peter"},
{ID: 200, Name: "Julia"},
}
func dbNewArticle(article *Article) (string, error) {
article.ID = fmt.Sprintf("%d", rand.Intn(100)+10)
articles = append(articles, article)
return article.ID, nil
}
func dbGetArticle(id string) (*Article, error) {
for _, a := range articles {
if a.ID == id {
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbGetArticleBySlug(slug string) (*Article, error) {
for _, a := range articles {
if a.Slug == slug {
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbUpdateArticle(id string, article *Article) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles[i] = article
return article, nil
}
}
return nil, errors.New("article not found.")
}
func dbRemoveArticle(id string) (*Article, error) {
for i, a := range articles {
if a.ID == id {
articles = append((articles)[:i], (articles)[i+1:]...)
return a, nil
}
}
return nil, errors.New("article not found.")
}
func dbGetUser(id int64) (*User, error) {
for _, u := range users {
if u.ID == id {
return u, nil
}
}
return nil, errors.New("user not found.")
}

260
_examples/rest/routes.json Normal file
View File

@@ -0,0 +1,260 @@
{
"router": {
"middlewares": [
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "RequestID",
"comment": "RequestID is a middleware that injects a request ID into the context of each\nrequest. A request ID is a string of the form \"host.example.com/random-0001\",\nwhere \"random\" is a base62 random string that uniquely identifies this go\nprocess, and where the last number is an atomically incremented request\ncounter.\n",
"file": "github.com/go-chi/chi/middleware/request_id.go",
"line": 63
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "Logger",
"comment": "Logger is a middleware that logs the start and end of each request, along\nwith some useful data about what was requested, what the response status was,\nand how long it took to return. When standard output is a TTY, Logger will\nprint in color, otherwise it will print in black and white. Logger prints a\nrequest ID if one is provided.\n\nAlternatively, look at https://github.com/pressly/lg and the `lg.RequestLogger`\nmiddleware pkg.\n",
"file": "github.com/go-chi/chi/middleware/logger.go",
"line": 26
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "Recoverer",
"comment": "Recoverer is a middleware that recovers from panics, logs the panic (and a\nbacktrace), and returns a HTTP 500 (Internal Server Error) status if\npossible. Recoverer prints a request ID if one is provided.\n\nAlternatively, look at https://github.com/pressly/lg middleware pkgs.\n",
"file": "github.com/go-chi/chi/middleware/recoverer.go",
"line": 18
},
{
"pkg": "github.com/go-chi/chi/v5/middleware",
"func": "URLFormat",
"comment": "URLFormat is a middleware that parses the url extension from a request path and stores it\non the context as a string under the key `middleware.URLFormatCtxKey`. The middleware will\ntrim the suffix from the routing path and continue routing.\n\nRouters should not include a url parameter for the suffix when using this middleware.\n\nSample usage.. for url paths: `/articles/1`, `/articles/1.json` and `/articles/1.xml`\n\n func routes() http.Handler {\n r := chi.NewRouter()\n r.Use(middleware.URLFormat)\n\n r.Get(\"/articles/{id}\", ListArticles)\n\n return r\n }\n\n func ListArticles(w http.ResponseWriter, r *http.Request) {\n\t urlFormat, _ := r.Context().Value(middleware.URLFormatCtxKey).(string)\n\n\t switch urlFormat {\n\t case \"json\":\n\t \trender.JSON(w, r, articles)\n\t case \"xml:\"\n\t \trender.XML(w, r, articles)\n\t default:\n\t \trender.JSON(w, r, articles)\n\t }\n}\n",
"file": "github.com/go-chi/chi/middleware/url_format.go",
"line": 45
},
{
"pkg": "github.com/go-chi/render",
"func": "SetContentType.func1",
"comment": "",
"file": "github.com/go-chi/render/content_type.go",
"line": 49,
"anonymous": true
}
],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func1",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 69,
"anonymous": true
}
}
},
"/admin/*": {
"router": {
"middlewares": [
{
"pkg": "",
"func": "main.AdminOnly",
"comment": "AdminOnly middleware restricts access to just administrators.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 238
}
],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func1",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 225,
"anonymous": true
}
}
},
"/accounts": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func2",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 228,
"anonymous": true
}
}
},
"/users/{userId}": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.adminRouter.func3",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 231,
"anonymous": true
}
}
}
}
}
},
"/articles/*": {
"router": {
"middlewares": [],
"routes": {
"/": {
"handlers": {
"GET": {
"middlewares": [
{
"pkg": "",
"func": "main.paginate",
"comment": "paginate is a stub, but very possible to implement middleware logic\nto handle the request params for handling a paginated request.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 251
}
],
"method": "GET",
"pkg": "",
"func": "main.ListArticles",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 117
},
"POST": {
"middlewares": [],
"method": "POST",
"pkg": "",
"func": "main.CreateArticle",
"comment": "CreateArticle persists the posted Article and returns it\nback to the client as an acknowledgement.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 158
}
}
},
"/search": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.SearchArticles",
"comment": "SearchArticles searches the Articles data for a matching article.\nIt's just a stub, but you get the idea.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 152
}
}
},
"/{articleID}/*": {
"router": {
"middlewares": [
{
"pkg": "",
"func": "main.ArticleCtx",
"comment": "ArticleCtx middleware is used to load an Article object from\nthe URL parameters passed through as the request. In case\nthe Article could not be found, we stop here and return a 404.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 127
}
],
"routes": {
"/": {
"handlers": {
"DELETE": {
"middlewares": [],
"method": "DELETE",
"pkg": "",
"func": "main.DeleteArticle",
"comment": "DeleteArticle removes an existing Article from our persistent store.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 204
},
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.GetArticle",
"comment": "GetArticle returns the specific Article. You'll notice it just\nfetches the Article right off the context, as its understood that\nif we made it this far, the Article must be on the context. In case\nits not due to a bug, then it will panic, and our Recoverer will save us.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 176
},
"PUT": {
"middlewares": [],
"method": "PUT",
"pkg": "",
"func": "main.UpdateArticle",
"comment": "UpdateArticle updates an existing Article in our persistent store.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 189
}
}
}
}
}
},
"/{articleSlug:[a-z-]+}": {
"handlers": {
"GET": {
"middlewares": [
{
"pkg": "",
"func": "main.ArticleCtx",
"comment": "ArticleCtx middleware is used to load an Article object from\nthe URL parameters passed through as the request. In case\nthe Article could not be found, we stop here and return a 404.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 127
}
],
"method": "GET",
"pkg": "",
"func": "main.GetArticle",
"comment": "GetArticle returns the specific Article. You'll notice it just\nfetches the Article right off the context, as its understood that\nif we made it this far, the Article must be on the context. In case\nits not due to a bug, then it will panic, and our Recoverer will save us.\n",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 176
}
}
}
}
}
},
"/panic": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func3",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 77,
"anonymous": true
}
}
},
"/ping": {
"handlers": {
"GET": {
"middlewares": [],
"method": "GET",
"pkg": "",
"func": "main.main.func2",
"comment": "",
"file": "github.com/go-chi/chi/_examples/rest/main.go",
"line": 73,
"anonymous": true
}
}
}
}
}
}

159
_examples/rest/routes.md Normal file
View File

@@ -0,0 +1,159 @@
# github.com/go-chi/chi
Welcome to the chi/_examples/rest generated docs.
## Routes
<details>
<summary>`/`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/**
- _GET_
- [main.main.func1](/_examples/rest/main.go#L69)
</details>
<details>
<summary>`/admin/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/**
- _GET_
- [main.adminRouter.func1](/_examples/rest/main.go#L225)
</details>
<details>
<summary>`/admin/*/accounts`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/accounts**
- _GET_
- [main.adminRouter.func2](/_examples/rest/main.go#L228)
</details>
<details>
<summary>`/admin/*/users/{userId}`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/admin/***
- [main.AdminOnly](/_examples/rest/main.go#L238)
- **/users/{userId}**
- _GET_
- [main.adminRouter.func3](/_examples/rest/main.go#L231)
</details>
<details>
<summary>`/articles/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/**
- _GET_
- [main.paginate](/_examples/rest/main.go#L251)
- [main.ListArticles](/_examples/rest/main.go#L117)
- _POST_
- [main.CreateArticle](/_examples/rest/main.go#L158)
</details>
<details>
<summary>`/articles/*/search`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/search**
- _GET_
- [main.SearchArticles](/_examples/rest/main.go#L152)
</details>
<details>
<summary>`/articles/*/{articleID}/*`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/{articleID}/***
- [main.ArticleCtx](/_examples/rest/main.go#L127)
- **/**
- _DELETE_
- [main.DeleteArticle](/_examples/rest/main.go#L204)
- _GET_
- [main.GetArticle](/_examples/rest/main.go#L176)
- _PUT_
- [main.UpdateArticle](/_examples/rest/main.go#L189)
</details>
<details>
<summary>`/articles/*/{articleSlug:[a-z-]+}`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/articles/***
- **/{articleSlug:[a-z-]+}**
- _GET_
- [main.ArticleCtx](/_examples/rest/main.go#L127)
- [main.GetArticle](/_examples/rest/main.go#L176)
</details>
<details>
<summary>`/panic`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/panic**
- _GET_
- [main.main.func3](/_examples/rest/main.go#L77)
</details>
<details>
<summary>`/ping`</summary>
- [RequestID](/middleware/request_id.go#L63)
- [Logger](/middleware/logger.go#L26)
- [Recoverer](/middleware/recoverer.go#L18)
- [URLFormat](/middleware/url_format.go#L45)
- [SetContentType.func1](https://github.com/go-chi/render/content_type.go#L49)
- **/ping**
- _GET_
- [main.main.func2](/_examples/rest/main.go#L73)
</details>
Total # of routes: 10

View File

@@ -0,0 +1,42 @@
package main
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("root."))
})
r.Route("/road", func(r chi.Router) {
r.Get("/left", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("left road"))
})
r.Post("/right", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("right road"))
})
})
r.Put("/ping", Ping)
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
route = strings.ReplaceAll(route, "/*/", "/")
fmt.Printf("%s %s\n", method, route)
return nil
}
if err := chi.Walk(r, walkFunc); err != nil {
fmt.Printf("Logging err: %s\n", err.Error())
}
}
// Ping returns pong
func Ping(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
}

View File

@@ -0,0 +1,30 @@
// This example demonstrates a project structure that defines a subrouter and its
// handlers on a struct, and mounting them as subrouters to a parent router.
// See also _examples/rest for an in-depth example of a REST service, and apply
// those same patterns to this structure.
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.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("."))
})
r.Mount("/users", usersResource{}.Routes())
r.Mount("/todos", todosResource{}.Routes())
http.ListenAndServe(":3333", r)
}

View File

@@ -0,0 +1,53 @@
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
type todosResource struct{}
// Routes creates a REST router for the todos resource
func (rs todosResource) Routes() chi.Router {
r := chi.NewRouter()
// r.Use() // some middleware..
r.Get("/", rs.List) // GET /todos - read a list of todos
r.Post("/", rs.Create) // POST /todos - create a new todo and persist it
r.Put("/", rs.Delete)
r.Route("/{id}", func(r chi.Router) {
// r.Use(rs.TodoCtx) // lets have a todos map, and lets actually load/manipulate
r.Get("/", rs.Get) // GET /todos/{id} - read a single todo by :id
r.Put("/", rs.Update) // PUT /todos/{id} - update a single todo by :id
r.Delete("/", rs.Delete) // DELETE /todos/{id} - delete a single todo by :id
r.Get("/sync", rs.Sync)
})
return r
}
func (rs todosResource) List(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todos list of stuff.."))
}
func (rs todosResource) Create(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todos create"))
}
func (rs todosResource) Get(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo get"))
}
func (rs todosResource) Update(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo update"))
}
func (rs todosResource) Delete(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo delete"))
}
func (rs todosResource) Sync(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("todo sync"))
}

View File

@@ -0,0 +1,48 @@
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
type usersResource struct{}
// Routes creates a REST router for the todos resource
func (rs usersResource) Routes() chi.Router {
r := chi.NewRouter()
// r.Use() // some middleware..
r.Get("/", rs.List) // GET /users - read a list of users
r.Post("/", rs.Create) // POST /users - create a new user and persist it
r.Put("/", rs.Delete)
r.Route("/{id}", func(r chi.Router) {
// r.Use(rs.TodoCtx) // lets have a users map, and lets actually load/manipulate
r.Get("/", rs.Get) // GET /users/{id} - read a single user by :id
r.Put("/", rs.Update) // PUT /users/{id} - update a single user by :id
r.Delete("/", rs.Delete) // DELETE /users/{id} - delete a single user by :id
})
return r
}
func (rs usersResource) List(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users list of stuff.."))
}
func (rs usersResource) Create(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users create"))
}
func (rs usersResource) Get(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user get"))
}
func (rs usersResource) Update(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user update"))
}
func (rs usersResource) Delete(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("user delete"))
}

View File

@@ -0,0 +1,9 @@
package data
// Article is runtime object, that's not meant to be sent via REST.
type Article struct {
ID int `db:"id" json:"id" xml:"id"`
Title string `db:"title" json:"title" xml:"title"`
Data []string `db:"data,stringarray" json:"data" xml:"data"`
CustomDataForAuthUsers string `db:"custom_data" json:"-" xml:"-"`
}

View File

@@ -0,0 +1,28 @@
package data
import (
"errors"
"net/http"
"github.com/go-chi/render"
)
var (
ErrUnauthorized = errors.New("Unauthorized")
ErrForbidden = errors.New("Forbidden")
ErrNotFound = errors.New("Resource not found")
)
func PresentError(r *http.Request, err error) (*http.Request, interface{}) {
switch err {
case ErrUnauthorized:
render.Status(r, 401)
case ErrForbidden:
render.Status(r, 403)
case ErrNotFound:
render.Status(r, 404)
default:
render.Status(r, 500)
}
return r, map[string]string{"error": err.Error()}
}

10
_examples/versions/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module versions
go 1.16
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/render v1.0.3
)
require github.com/ajg/form v1.5.1 // indirect

View File

@@ -0,0 +1,6 @@
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=

154
_examples/versions/main.go Normal file
View File

@@ -0,0 +1,154 @@
// This example demonstrates the use of the render subpackage, with
// a quick concept for how to support multiple api versions.
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/_examples/versions/data"
v1 "github.com/go-chi/chi/v5/_examples/versions/presenter/v1"
v2 "github.com/go-chi/chi/v5/_examples/versions/presenter/v2"
v3 "github.com/go-chi/chi/v5/_examples/versions/presenter/v3"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// API version 3.
r.Route("/v3", func(r chi.Router) {
r.Use(apiVersionCtx("v3"))
r.Mount("/articles", articleRouter())
})
// API version 2.
r.Route("/v2", func(r chi.Router) {
r.Use(apiVersionCtx("v2"))
r.Mount("/articles", articleRouter())
})
// API version 1.
r.Route("/v1", func(r chi.Router) {
r.Use(randomErrorMiddleware) // Simulate random error, ie. version 1 is buggy.
r.Use(apiVersionCtx("v1"))
r.Mount("/articles", articleRouter())
})
http.ListenAndServe(":3333", r)
}
func apiVersionCtx(version string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "api.version", version))
next.ServeHTTP(w, r)
})
}
}
func articleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", listArticles)
r.Route("/{articleID}", func(r chi.Router) {
r.Get("/", getArticle)
// r.Put("/", updateArticle)
// r.Delete("/", deleteArticle)
})
return r
}
func listArticles(w http.ResponseWriter, r *http.Request) {
articles := make(chan render.Renderer, 5)
// Load data asynchronously into the channel (simulate slow storage):
go func() {
for i := 1; i <= 10; i++ {
article := &data.Article{
ID: i,
Title: fmt.Sprintf("Article #%v", i),
Data: []string{"one", "two", "three", "four"},
CustomDataForAuthUsers: "secret data for auth'd users only",
}
apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
articles <- v1.NewArticleResponse(article)
case "v2":
articles <- v2.NewArticleResponse(article)
default:
articles <- v3.NewArticleResponse(article)
}
time.Sleep(100 * time.Millisecond)
}
close(articles)
}()
// Start streaming data from the channel.
render.Respond(w, r, articles)
}
func getArticle(w http.ResponseWriter, r *http.Request) {
// Load article.
if chi.URLParam(r, "articleID") != "1" {
render.Respond(w, r, data.ErrNotFound)
return
}
article := &data.Article{
ID: 1,
Title: "Article #1",
Data: []string{"one", "two", "three", "four"},
CustomDataForAuthUsers: "secret data for auth'd users only",
}
// Simulate some context values:
// 1. ?auth=true simulates authenticated session/user.
// 2. ?error=true simulates random error.
if r.URL.Query().Get("auth") != "" {
r = r.WithContext(context.WithValue(r.Context(), "auth", true))
}
if r.URL.Query().Get("error") != "" {
render.Respond(w, r, errors.New("error"))
return
}
var payload render.Renderer
apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
payload = v1.NewArticleResponse(article)
case "v2":
payload = v2.NewArticleResponse(article)
default:
payload = v3.NewArticleResponse(article)
}
render.Render(w, r, payload)
}
func randomErrorMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
// One in three chance of random error.
if rand.Int31n(3) == 0 {
errors := []error{data.ErrUnauthorized, data.ErrForbidden, data.ErrNotFound}
render.Respond(w, r, errors[rand.Intn(len(errors))])
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,22 @@
package v1
import (
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 1.
type Article struct {
*data.Article
Data map[string]bool `json:"data" xml:"data"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}

View File

@@ -0,0 +1,30 @@
package v2
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 2.
type Article struct {
// *v3.Article `json:",inline" xml:",inline"`
*data.Article
// Additional fields.
SelfURL string `json:"self_url" xml:"self_url"`
// Omitted fields.
URL interface{} `json:"url,omitempty" xml:"url,omitempty"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.SelfURL = fmt.Sprintf("http://localhost:3333/v2?id=%v", a.ID)
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}

View File

@@ -0,0 +1,39 @@
package v3
import (
"fmt"
"math/rand"
"net/http"
"github.com/go-chi/chi/v5/_examples/versions/data"
)
// Article presented in API version 2.
type Article struct {
*data.Article `json:",inline" xml:",inline"`
// Additional fields.
URL string `json:"url" xml:"url"`
ViewsCount int64 `json:"views_count" xml:"views_count"`
APIVersion string `json:"api_version" xml:"api_version"`
// Omitted fields.
// Show custom_data explicitly for auth'd users only.
CustomDataForAuthUsers interface{} `json:"custom_data,omitempty" xml:"custom_data,omitempty"`
}
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.ViewsCount = rand.Int63n(100000)
a.URL = fmt.Sprintf("http://localhost:3333/v3/?id=%v", a.ID)
// Only show to auth'd user.
if _, ok := r.Context().Value("auth").(bool); ok {
a.CustomDataForAuthUsers = a.Article.CustomDataForAuthUsers
}
return nil
}
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}