1// 2// REST 3// ==== 4// This example demonstrates a HTTP REST web service with some fixture data. 5// Follow along the example and patterns. 6// 7// Also check routes.json for the generated docs from passing the -routes flag 8// 9// Boot the server: 10// ---------------- 11// $ go run main.go 12// 13// Client requests: 14// ---------------- 15// $ curl http://localhost:3333/ 16// root. 17// 18// $ curl http://localhost:3333/articles 19// [{"id":"1","title":"Hi"},{"id":"2","title":"sup"}] 20// 21// $ curl http://localhost:3333/articles/1 22// {"id":"1","title":"Hi"} 23// 24// $ curl -X DELETE http://localhost:3333/articles/1 25// {"id":"1","title":"Hi"} 26// 27// $ curl http://localhost:3333/articles/1 28// "Not Found" 29// 30// $ curl -X POST -d '{"id":"will-be-omitted","title":"awesomeness"}' http://localhost:3333/articles 31// {"id":"97","title":"awesomeness"} 32// 33// $ curl http://localhost:3333/articles/97 34// {"id":"97","title":"awesomeness"} 35// 36// $ curl http://localhost:3333/articles 37// [{"id":"2","title":"sup"},{"id":"97","title":"awesomeness"}] 38// 39package main 40 41import ( 42 "context" 43 "errors" 44 "flag" 45 "fmt" 46 "math/rand" 47 "net/http" 48 "strings" 49 50 "github.com/go-chi/chi" 51 "github.com/go-chi/chi/middleware" 52 "github.com/go-chi/docgen" 53 "github.com/go-chi/render" 54) 55 56var routes = flag.Bool("routes", false, "Generate router documentation") 57 58func main() { 59 flag.Parse() 60 61 r := chi.NewRouter() 62 63 r.Use(middleware.RequestID) 64 r.Use(middleware.Logger) 65 r.Use(middleware.Recoverer) 66 r.Use(middleware.URLFormat) 67 r.Use(render.SetContentType(render.ContentTypeJSON)) 68 69 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 70 w.Write([]byte("root.")) 71 }) 72 73 r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 74 w.Write([]byte("pong")) 75 }) 76 77 r.Get("/panic", func(w http.ResponseWriter, r *http.Request) { 78 panic("test") 79 }) 80 81 // RESTy routes for "articles" resource 82 r.Route("/articles", func(r chi.Router) { 83 r.With(paginate).Get("/", ListArticles) 84 r.Post("/", CreateArticle) // POST /articles 85 r.Get("/search", SearchArticles) // GET /articles/search 86 87 r.Route("/{articleID}", func(r chi.Router) { 88 r.Use(ArticleCtx) // Load the *Article on the request context 89 r.Get("/", GetArticle) // GET /articles/123 90 r.Put("/", UpdateArticle) // PUT /articles/123 91 r.Delete("/", DeleteArticle) // DELETE /articles/123 92 }) 93 94 // GET /articles/whats-up 95 r.With(ArticleCtx).Get("/{articleSlug:[a-z-]+}", GetArticle) 96 }) 97 98 // Mount the admin sub-router, which btw is the same as: 99 // r.Route("/admin", func(r chi.Router) { admin routes here }) 100 r.Mount("/admin", adminRouter()) 101 102 // Passing -routes to the program will generate docs for the above 103 // router definition. See the `routes.json` file in this folder for 104 // the output. 105 if *routes { 106 // fmt.Println(docgen.JSONRoutesDoc(r)) 107 fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{ 108 ProjectPath: "github.com/go-chi/chi", 109 Intro: "Welcome to the chi/_examples/rest generated docs.", 110 })) 111 return 112 } 113 114 http.ListenAndServe(":3333", r) 115} 116 117func ListArticles(w http.ResponseWriter, r *http.Request) { 118 if err := render.RenderList(w, r, NewArticleListResponse(articles)); err != nil { 119 render.Render(w, r, ErrRender(err)) 120 return 121 } 122} 123 124// ArticleCtx middleware is used to load an Article object from 125// the URL parameters passed through as the request. In case 126// the Article could not be found, we stop here and return a 404. 127func ArticleCtx(next http.Handler) http.Handler { 128 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 var article *Article 130 var err error 131 132 if articleID := chi.URLParam(r, "articleID"); articleID != "" { 133 article, err = dbGetArticle(articleID) 134 } else if articleSlug := chi.URLParam(r, "articleSlug"); articleSlug != "" { 135 article, err = dbGetArticleBySlug(articleSlug) 136 } else { 137 render.Render(w, r, ErrNotFound) 138 return 139 } 140 if err != nil { 141 render.Render(w, r, ErrNotFound) 142 return 143 } 144 145 ctx := context.WithValue(r.Context(), "article", article) 146 next.ServeHTTP(w, r.WithContext(ctx)) 147 }) 148} 149 150// SearchArticles searches the Articles data for a matching article. 151// It's just a stub, but you get the idea. 152func SearchArticles(w http.ResponseWriter, r *http.Request) { 153 render.RenderList(w, r, NewArticleListResponse(articles)) 154} 155 156// CreateArticle persists the posted Article and returns it 157// back to the client as an acknowledgement. 158func CreateArticle(w http.ResponseWriter, r *http.Request) { 159 data := &ArticleRequest{} 160 if err := render.Bind(r, data); err != nil { 161 render.Render(w, r, ErrInvalidRequest(err)) 162 return 163 } 164 165 article := data.Article 166 dbNewArticle(article) 167 168 render.Status(r, http.StatusCreated) 169 render.Render(w, r, NewArticleResponse(article)) 170} 171 172// GetArticle returns the specific Article. You'll notice it just 173// fetches the Article right off the context, as its understood that 174// if we made it this far, the Article must be on the context. In case 175// its not due to a bug, then it will panic, and our Recoverer will save us. 176func GetArticle(w http.ResponseWriter, r *http.Request) { 177 // Assume if we've reach this far, we can access the article 178 // context because this handler is a child of the ArticleCtx 179 // middleware. The worst case, the recoverer middleware will save us. 180 article := r.Context().Value("article").(*Article) 181 182 if err := render.Render(w, r, NewArticleResponse(article)); err != nil { 183 render.Render(w, r, ErrRender(err)) 184 return 185 } 186} 187 188// UpdateArticle updates an existing Article in our persistent store. 189func UpdateArticle(w http.ResponseWriter, r *http.Request) { 190 article := r.Context().Value("article").(*Article) 191 192 data := &ArticleRequest{Article: article} 193 if err := render.Bind(r, data); err != nil { 194 render.Render(w, r, ErrInvalidRequest(err)) 195 return 196 } 197 article = data.Article 198 dbUpdateArticle(article.ID, article) 199 200 render.Render(w, r, NewArticleResponse(article)) 201} 202 203// DeleteArticle removes an existing Article from our persistent store. 204func DeleteArticle(w http.ResponseWriter, r *http.Request) { 205 var err error 206 207 // Assume if we've reach this far, we can access the article 208 // context because this handler is a child of the ArticleCtx 209 // middleware. The worst case, the recoverer middleware will save us. 210 article := r.Context().Value("article").(*Article) 211 212 article, err = dbRemoveArticle(article.ID) 213 if err != nil { 214 render.Render(w, r, ErrInvalidRequest(err)) 215 return 216 } 217 218 render.Render(w, r, NewArticleResponse(article)) 219} 220 221// A completely separate router for administrator routes 222func adminRouter() chi.Router { 223 r := chi.NewRouter() 224 r.Use(AdminOnly) 225 r.Get("/", func(w http.ResponseWriter, r *http.Request) { 226 w.Write([]byte("admin: index")) 227 }) 228 r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) { 229 w.Write([]byte("admin: list accounts..")) 230 }) 231 r.Get("/users/{userId}", func(w http.ResponseWriter, r *http.Request) { 232 w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(r, "userId")))) 233 }) 234 return r 235} 236 237// AdminOnly middleware restricts access to just administrators. 238func AdminOnly(next http.Handler) http.Handler { 239 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 240 isAdmin, ok := r.Context().Value("acl.admin").(bool) 241 if !ok || !isAdmin { 242 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) 243 return 244 } 245 next.ServeHTTP(w, r) 246 }) 247} 248 249// paginate is a stub, but very possible to implement middleware logic 250// to handle the request params for handling a paginated request. 251func paginate(next http.Handler) http.Handler { 252 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 253 // just a stub.. some ideas are to look at URL query params for something like 254 // the page number, or the limit, and send a query cursor down the chain 255 next.ServeHTTP(w, r) 256 }) 257} 258 259// This is entirely optional, but I wanted to demonstrate how you could easily 260// add your own logic to the render.Respond method. 261func init() { 262 render.Respond = func(w http.ResponseWriter, r *http.Request, v interface{}) { 263 if err, ok := v.(error); ok { 264 265 // We set a default error status response code if one hasn't been set. 266 if _, ok := r.Context().Value(render.StatusCtxKey).(int); !ok { 267 w.WriteHeader(400) 268 } 269 270 // We log the error 271 fmt.Printf("Logging err: %s\n", err.Error()) 272 273 // We change the response to not reveal the actual error message, 274 // instead we can transform the message something more friendly or mapped 275 // to some code / language, etc. 276 render.DefaultResponder(w, r, render.M{"status": "error"}) 277 return 278 } 279 280 render.DefaultResponder(w, r, v) 281 } 282} 283 284//-- 285// Request and Response payloads for the REST api. 286// 287// The payloads embed the data model objects an 288// 289// In a real-world project, it would make sense to put these payloads 290// in another file, or another sub-package. 291//-- 292 293type UserPayload struct { 294 *User 295 Role string `json:"role"` 296} 297 298func NewUserPayloadResponse(user *User) *UserPayload { 299 return &UserPayload{User: user} 300} 301 302// Bind on UserPayload will run after the unmarshalling is complete, its 303// a good time to focus some post-processing after a decoding. 304func (u *UserPayload) Bind(r *http.Request) error { 305 return nil 306} 307 308func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) error { 309 u.Role = "collaborator" 310 return nil 311} 312 313// ArticleRequest is the request payload for Article data model. 314// 315// NOTE: It's good practice to have well defined request and response payloads 316// so you can manage the specific inputs and outputs for clients, and also gives 317// you the opportunity to transform data on input or output, for example 318// on request, we'd like to protect certain fields and on output perhaps 319// we'd like to include a computed field based on other values that aren't 320// in the data model. Also, check out this awesome blog post on struct composition: 321// http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/ 322type ArticleRequest struct { 323 *Article 324 325 User *UserPayload `json:"user,omitempty"` 326 327 ProtectedID string `json:"id"` // override 'id' json to have more control 328} 329 330func (a *ArticleRequest) Bind(r *http.Request) error { 331 // a.Article is nil if no Article fields are sent in the request. Return an 332 // error to avoid a nil pointer dereference. 333 if a.Article == nil { 334 return errors.New("missing required Article fields.") 335 } 336 337 // a.User is nil if no Userpayload fields are sent in the request. In this app 338 // this won't cause a panic, but checks in this Bind method may be required if 339 // a.User or futher nested fields like a.User.Name are accessed elsewhere. 340 341 // just a post-process after a decode.. 342 a.ProtectedID = "" // unset the protected ID 343 a.Article.Title = strings.ToLower(a.Article.Title) // as an example, we down-case 344 return nil 345} 346 347// ArticleResponse is the response payload for the Article data model. 348// See NOTE above in ArticleRequest as well. 349// 350// In the ArticleResponse object, first a Render() is called on itself, 351// then the next field, and so on, all the way down the tree. 352// Render is called in top-down order, like a http handler middleware chain. 353type ArticleResponse struct { 354 *Article 355 356 User *UserPayload `json:"user,omitempty"` 357 358 // We add an additional field to the response here.. such as this 359 // elapsed computed property 360 Elapsed int64 `json:"elapsed"` 361} 362 363func NewArticleResponse(article *Article) *ArticleResponse { 364 resp := &ArticleResponse{Article: article} 365 366 if resp.User == nil { 367 if user, _ := dbGetUser(resp.UserID); user != nil { 368 resp.User = NewUserPayloadResponse(user) 369 } 370 } 371 372 return resp 373} 374 375func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Request) error { 376 // Pre-processing before a response is marshalled and sent across the wire 377 rd.Elapsed = 10 378 return nil 379} 380 381func NewArticleListResponse(articles []*Article) []render.Renderer { 382 list := []render.Renderer{} 383 for _, article := range articles { 384 list = append(list, NewArticleResponse(article)) 385 } 386 return list 387} 388 389// NOTE: as a thought, the request and response payloads for an Article could be the 390// same payload type, perhaps will do an example with it as well. 391// type ArticlePayload struct { 392// *Article 393// } 394 395//-- 396// Error response payloads & renderers 397//-- 398 399// ErrResponse renderer type for handling all sorts of errors. 400// 401// In the best case scenario, the excellent github.com/pkg/errors package 402// helps reveal information on the error, setting it on Err, and in the Render() 403// method, using it to set the application-specific error code in AppCode. 404type ErrResponse struct { 405 Err error `json:"-"` // low-level runtime error 406 HTTPStatusCode int `json:"-"` // http response status code 407 408 StatusText string `json:"status"` // user-level status message 409 AppCode int64 `json:"code,omitempty"` // application-specific error code 410 ErrorText string `json:"error,omitempty"` // application-level error message, for debugging 411} 412 413func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { 414 render.Status(r, e.HTTPStatusCode) 415 return nil 416} 417 418func ErrInvalidRequest(err error) render.Renderer { 419 return &ErrResponse{ 420 Err: err, 421 HTTPStatusCode: 400, 422 StatusText: "Invalid request.", 423 ErrorText: err.Error(), 424 } 425} 426 427func ErrRender(err error) render.Renderer { 428 return &ErrResponse{ 429 Err: err, 430 HTTPStatusCode: 422, 431 StatusText: "Error rendering response.", 432 ErrorText: err.Error(), 433 } 434} 435 436var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."} 437 438//-- 439// Data model objects and persistence mocks: 440//-- 441 442// User data model 443type User struct { 444 ID int64 `json:"id"` 445 Name string `json:"name"` 446} 447 448// Article data model. I suggest looking at https://upper.io for an easy 449// and powerful data persistence adapter. 450type Article struct { 451 ID string `json:"id"` 452 UserID int64 `json:"user_id"` // the author 453 Title string `json:"title"` 454 Slug string `json:"slug"` 455} 456 457// Article fixture data 458var articles = []*Article{ 459 {ID: "1", UserID: 100, Title: "Hi", Slug: "hi"}, 460 {ID: "2", UserID: 200, Title: "sup", Slug: "sup"}, 461 {ID: "3", UserID: 300, Title: "alo", Slug: "alo"}, 462 {ID: "4", UserID: 400, Title: "bonjour", Slug: "bonjour"}, 463 {ID: "5", UserID: 500, Title: "whats up", Slug: "whats-up"}, 464} 465 466// User fixture data 467var users = []*User{ 468 {ID: 100, Name: "Peter"}, 469 {ID: 200, Name: "Julia"}, 470} 471 472func dbNewArticle(article *Article) (string, error) { 473 article.ID = fmt.Sprintf("%d", rand.Intn(100)+10) 474 articles = append(articles, article) 475 return article.ID, nil 476} 477 478func dbGetArticle(id string) (*Article, error) { 479 for _, a := range articles { 480 if a.ID == id { 481 return a, nil 482 } 483 } 484 return nil, errors.New("article not found.") 485} 486 487func dbGetArticleBySlug(slug string) (*Article, error) { 488 for _, a := range articles { 489 if a.Slug == slug { 490 return a, nil 491 } 492 } 493 return nil, errors.New("article not found.") 494} 495 496func dbUpdateArticle(id string, article *Article) (*Article, error) { 497 for i, a := range articles { 498 if a.ID == id { 499 articles[i] = article 500 return article, nil 501 } 502 } 503 return nil, errors.New("article not found.") 504} 505 506func dbRemoveArticle(id string) (*Article, error) { 507 for i, a := range articles { 508 if a.ID == id { 509 articles = append((articles)[:i], (articles)[i+1:]...) 510 return a, nil 511 } 512 } 513 return nil, errors.New("article not found.") 514} 515 516func dbGetUser(id int64) (*User, error) { 517 for _, u := range users { 518 if u.ID == id { 519 return u, nil 520 } 521 } 522 return nil, errors.New("user not found.") 523} 524