// // REST // ==== // 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 // // 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" "github.com/go-chi/chi/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", 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 futher 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.") }