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