Last active
May 3, 2023 10:10
-
-
Save utamori/2c66a969031e79e2e85c74187a3b41aa to your computer and use it in GitHub Desktop.
go-chiでrest api作る例
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
}) | |
// "articles"リソースへのREST的なルーティング | |
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) | |
}) | |
// adminサブルーターをマウントします。これは次と同じです: | |
// 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 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.") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment