...

Source file src/github.com/go-chi/chi/_examples/rest/main.go

Documentation: github.com/go-chi/chi/_examples/rest

     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  //
    39  package main
    40  
    41  import (
    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  
    56  var routes = flag.Bool("routes", false, "Generate router documentation")
    57  
    58  func 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  
   117  func 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.
   127  func 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.
   152  func 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.
   158  func 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.
   176  func 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.
   189  func 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.
   204  func 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
   222  func 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.
   238  func 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.
   251  func 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.
   261  func 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  
   293  type UserPayload struct {
   294  	*User
   295  	Role string `json:"role"`
   296  }
   297  
   298  func 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.
   304  func (u *UserPayload) Bind(r *http.Request) error {
   305  	return nil
   306  }
   307  
   308  func (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/
   322  type 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  
   330  func (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.
   353  type 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  
   363  func 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  
   375  func (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  
   381  func 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.
   404  type 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  
   413  func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
   414  	render.Status(r, e.HTTPStatusCode)
   415  	return nil
   416  }
   417  
   418  func 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  
   427  func 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  
   436  var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
   437  
   438  //--
   439  // Data model objects and persistence mocks:
   440  //--
   441  
   442  // User data model
   443  type 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.
   450  type 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
   458  var 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
   467  var users = []*User{
   468  	{ID: 100, Name: "Peter"},
   469  	{ID: 200, Name: "Julia"},
   470  }
   471  
   472  func 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  
   478  func 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  
   487  func 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  
   496  func 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  
   506  func 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  
   516  func 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  

View as plain text