diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..b8253638 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,76 @@ +# Chi + +## ๐Ÿ‘‹ Hi, Let's Get You Started With Chi + + + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](https://github.com/go-chi/chi/tree/master/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](https://github.com/go-chi/chi#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - as of v5, go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + + +## Examples + +See [examples](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 00000000..a7a1fc07 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,15 @@ + + + + +- [๐Ÿ“š User Guide](user_guide/index.md) + - [๐Ÿ‘‹ First Steps](user_guide/first_steps.md) + - [๐Ÿ”Œ Routing](user_guide/routing.md) + - [๐Ÿงฌ Middleware](user_guide/middleware.md) + - [๐Ÿงช Testing](user_guide/testing.md) + - [๐Ÿณ Examples](https://github.com/go-chi/chi/tree/master/_examples) \ No newline at end of file diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100755 index 00000000..59285a08 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..a9d83e5a --- /dev/null +++ b/docs/index.html @@ -0,0 +1,52 @@ + + + + + Chi + + + + + + + + + + + + + + +
Loading ...
+ + + + + + + + + diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..3ac7fa73 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,455 @@ + + +# Quick Start + +This tutorial shows how to use `chi` in a simple crud API. +This tutorial is only to show you how an api would look with chi. + +## Installation + +`go get -u github.com/go-chi/chi/v5` + + +## Running a Simple Server + +The simplest Hello World Api Can look like this. + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + }) + http.ListenAndServe(":3000", r) +} +``` +```sh +go run main.go +``` +Browse to `http://localhost:3000`, and you should see `Hello World!` on the page. + +## Adding Usefull Middleware +```go +package main + +import ( + //... + "net/http" + "time" + + "context" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + + +func main(){ + r := chi.NewRouter() + + + r.Use(middleware.RequestID) + + // RealIP is a middleware that sets a http.Request's RemoteAddr to the results + // of parsing either the X-Real-IP header or the X-Forwarded-For header (in that + // order). + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + + // Recoverer is a middleware that recovers from panics, logs the panic (and a + // backtrace), and returns a HTTP 500 (Internal Server Error) status if + // possible. Recoverer prints a request ID if one is provided. + r.Use(middleware.Recoverer) + + // CleanPath middleware will clean out double slash mistakes from a user's request path. + // For example, if a user requests /users//1 or //users////1 will both be treated as: /users/1 + r.Use(middleware.CleanPath) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + }) + +} +``` + +## Adding Simple CRUD Patterns + +### Creating a Simple Structs for Models +We will create a simple todo and User struct to work as a model +```go +// Todo struct is a model for creating todos +type Todo struct { + ID int32 `json:"id"` + Body string `json:"body"` + Username string `json:"username"` + Done bool `json:"done"` +} + +// User struct is a model for users +type User struct { + Username string `json:"username"` + Password string `json:"password"` +} +``` + +### Replicating a Temporary DB store +```go +// Store is used to replicate a database store +type Store struct { + Users map[string]User // username:User + TODOs map[int32]Todo // id:Todo +} + +// GetUser gets the User Model from Store by username +func (s *Store) GetUser(username string) (User, error) { + user, ok := s.Users[username] + if !ok { + return User{}, fmt.Errorf("user with username: %s, does not exist", username) + } + return user, nil +} + +// AddUser creates a new user and adds it to the store +func (s *Store) AddUser(user User) error { + _, ok := s.Users[user.Username] + if ok { + return fmt.Errorf("user with username: %s, already exists", user.Username) + } + s.Users[user.Username] = user + return nil +} + +// CreateTodo creates a new todo and stores it in the store +func (s *Store) CreateTodo(body string, username string) { + id := int32(len(s.TODOs) + 1) + s.TODOs[id] = Todo{ + ID: id, + Body: body, + Username: username, + Done: false, + } + fmt.Println(s.TODOs) +} + +// GetAllToDo gets all todos created by a user +func (s *Store) GetAllToDo(username string) (todos []Todo) { + fmt.Println(s.TODOs) + for i := 1; i <= len(s.TODOs); i++ { + todo := s.TODOs[int32(i)] + fmt.Println(todo, username) + if todo.Username == username { + fmt.Println(todo) + todos = append(todos, todo) + } + } + return +} + +// DeleteToDo deletes the todo from the store +func (s *Store) DeleteToDo(id int32) error { + _, ok := s.TODOs[id] + if !ok { + return fmt.Errorf("todo with id: %v, does not exist", id) + } + delete(s.TODOs, id) + return nil +} + +// ModifyToDo modifies the todo in the store +func (s *Store) ModifyToDo(id int32, body string, done bool, username string) error { + todo, ok := s.TODOs[id] + if !ok { + return fmt.Errorf("todo with id: %v, does not exist", id) + } + if todo.Username != username { + return fmt.Errorf("you are not the owner of this todo") + } + todo.Body = body + todo.Done = done + s.TODOs[id] = todo + return nil +} +``` +### Creating a Response Util +```go +// JSON returns a well formatted response with a status code +func JSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.WriteHeader(statusCode) + err := json.NewEncoder(w).Encode(data) + if err != nil { + w.WriteHeader(500) + er := json.NewEncoder(w).Encode(map[string]interface{}{"error": "something unexpected occurred."}) + if er != nil { + return + } + } +} +``` +### Creating Handlers +```go + +// SignUpHandler responsds to /auth/signup +// it is used for creating a user +func SignUpHandler(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{"error": "something unexpected occurred"} + + decoder := json.NewDecoder(r.Body) // "encoding/json" + params := &User{} + err := decoder.Decode(params) // Parsing request and storing it in a User Model + if err != nil { + log.Println(err) + JSON(w, 500, resp) + return + } + // Checking for empty username or password + if params.Username == "" || params.Password == "" { + resp["error"] = "username and password are required" + JSON(w, 400, resp) + return + } + err = store.AddUser(*params) + if err != nil { + resp["error"] = err.Error() + JSON(w, 400, resp) + } + JSON(w, 201, map[string]string{"response": "user created"}) + } +} + +// CreateToDoHandler responds to POST /todo/create +// it creates a new todo model +func CreateToDoHandler(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{"error": "something unexpected occurred"} + decoder := json.NewDecoder(r.Body) + params := &Todo{} + err := decoder.Decode(params) + if err != nil { + log.Println(err) + JSON(w, 500, resp) + return + } + if params.Body == "" { + resp["error"] = "body is required" + JSON(w, 400, resp) + return + } + + // Getting Username of Logged In User set by auth middleware + params.Username = r.Context().Value("username").(string) + store.CreateTodo(params.Body, params.Username) + JSON(w, 201, map[string]string{"response": "todo created"}) + } +} + +// GetAllToDoHandler responds to GET /todo/all +// returns all todo related to a user +func GetAllToDoHandler(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + todos := store.GetAllToDo(r.Context().Value("username").(string)) + JSON(w, 200, todos) + } +} + +// DeleteToDoHandler responds to DELETE /todo/delete/{id} +// it deletes the todo if it exists and was created by the logged in user +func DeleteToDoHandler(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var resp map[string]interface{} = map[string]interface{}{} + + id, err := strconv.Atoi(chi.URLParam(r, "id")) + if err != nil { + resp["error"] = "something unexpected occured" + JSON(w, http.StatusInternalServerError, resp) + return + } + todo, ok := store.TODOs[int32(id)] + if !ok { + resp["error"] = fmt.Sprintf("todo with id: %v, does not exist", id) + JSON(w, 400, resp) + return + } + if todo.Username != r.Context().Value("username").(string) { + resp["error"] = "you did not create this todo" + JSON(w, 400, resp) + return + } + delete(store.TODOs, int32(id)) + resp["response"] = "deleted todo" + JSON(w, 200, resp) + } +} + +func ModifyToDoHandler(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{"error": "something unexpected occurred"} + decoder := json.NewDecoder(r.Body) + params := &Todo{} + err := decoder.Decode(params) + if err != nil { + log.Println(err) + JSON(w, 500, resp) + return + } + if params.Body == "" || params.ID == 0 { + resp["error"] = "body and id are required" + JSON(w, 400, resp) + return + } + + params.Username = r.Context().Value("username").(string) + err = store.ModifyToDo(params.ID, params.Body, params.Done, params.Username) + // store.CreateToDo(params.Body, params.Username) + if err != nil { + resp["error"] = err.Error() + JSON(w, 400, resp) + return + } + JSON(w, 201, map[string]string{"response": "todo modified"}) + } +} + +``` + +## Creating Middlewares + +### Auth Middleware +```go +// AuthMiddleware expects username and password in Header +// This is only to show you how to use middleware +// This is not at all meant for production +func AuthMiddleware(store *Store) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var resp = map[string]interface{}{"error": "unauthorized", "message": "missing authorization"} + var username = r.Header.Get("username") + var password = r.Header.Get("password") + username = strings.TrimSpace(username) + password = strings.TrimSpace(password) + if username == "" || password == "" { + JSON(w, http.StatusUnauthorized, resp) + return + } + // Confirming username and password are correct + user, err := store.GetUser(username) + if err != nil { + resp["message"] = err.Error() + JSON(w, http.StatusUnauthorized, resp) + return + } + if user.Password != password { + resp["message"] = "password is incorrect" + JSON(w, http.StatusUnauthorized, resp) + return + } + // Setting username in context + ctx := context.WithValue(r.Context(), "username", username) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +``` + +### Middleware for settings content-type +```go +// SetContentTypeMiddleware sets content-type to json +func SetContentTypeMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + next.ServeHTTP(w, r) + }) +} +``` + +## Adding Route Groups and Finishing it +```go +func main() { + r := chi.NewRouter() + + r.Use(middleware.RequestID) + + // RealIP is a middleware that sets a http.Request's RemoteAddr to the results + // of parsing either the X-Real-IP header or the X-Forwarded-For header (in that + // order). + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + + // Recoverer is a middleware that recovers from panics, logs the panic (and a + // backtrace), and returns a HTTP 500 (Internal Server Error) status if + // possible. Recoverer prints a request ID if one is provided. + r.Use(middleware.Recoverer) + + // CleanPath middleware will clean out double slash mistakes from a user's request path. + // For example, if a user requests /users//1 or //users////1 will both be treated as: /users/1 + r.Use(middleware.CleanPath) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + // Initializing our db store + store := Store{ + Users: map[string]User{}, + TODOs: map[int32]Todo{}, + } + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + }) + + // Creating a New Router for todo handlers + todoRouter := chi.NewRouter() + + // Use Content-Type Middleware + todoRouter.Use(SetContentTypeMiddleware) + + // Use Auth Middleware since these are protected Routes + todoRouter.Use(AuthMiddleware(&store)) + todoRouter.Get("/all", GetAllToDoHandler(&store)) + todoRouter.Post("/create", CreateToDoHandler(&store)) + todoRouter.Put("/modify", ModifyToDoHandler(&store)) + todoRouter.Delete("/delete/{id}", DeleteToDoHandler(&store)) + + // Creating New Router for User Authentication + userAuthRouter := chi.NewRouter() + userAuthRouter.Use(SetContentTypeMiddleware) + userAuthRouter.Post("/signup", SignUpHandler(&store)) + + // Mounting Both Sub Routers to a path in the main router + r.Mount("/todo", todoRouter) + r.Mount("/auth", userAuthRouter) + + // Starting the Server + http.ListenAndServe("localhost:5000", r) +} + +``` + +This was a small simple tutorial just to show you how to use the basic features +of `chi` + +To Learn More Visit [The Advanced User Guide](advanced_user_guide/index.md) \ No newline at end of file diff --git a/docs/user_guide/first_steps.md b/docs/user_guide/first_steps.md new file mode 100644 index 00000000..9832d60d --- /dev/null +++ b/docs/user_guide/first_steps.md @@ -0,0 +1,32 @@ +## Installation + +`go get -u github.com/go-chi/chi/v5` + + +## Running a Simple Server + +The simplest Hello World Api Can look like this. + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + }) + http.ListenAndServe(":3000", r) +} +``` +```sh +go run main.go +``` +Browse to `http://localhost:3000`, and you should see `Hello World!` on the page. diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md new file mode 100644 index 00000000..19a3b0c4 --- /dev/null +++ b/docs/user_guide/index.md @@ -0,0 +1,11 @@ +# ๐Ÿ“š User Guide + +This User Guide demonstrates all the features `chi` has. + +## Table of Contents + +1. [๐Ÿ‘‹ First Steps](user_guide/first_steps.md) +2. [๐Ÿ”Œ Routing](user_guide/routing.md) +3. [๐Ÿงฌ Middleware](user_guide/middleware.md) +4. [๐Ÿงช Testing](user_guide/testing.md) +5. [๐Ÿณ Examples](https://github.com/go-chi/chi/tree/master/_examples) \ No newline at end of file diff --git a/docs/user_guide/middleware.md b/docs/user_guide/middleware.md new file mode 100644 index 00000000..f26237a0 --- /dev/null +++ b/docs/user_guide/middleware.md @@ -0,0 +1,279 @@ +# ๐Ÿงฌ Middleware + +> Middleware performs some specific function on the HTTP request or response at a specific stage in the HTTP pipeline before or after the user defined controller. Middleware is a design pattern to eloquently add cross cutting concerns like logging, handling authentication without having many code contact points. + + +`chi's` middlewares are just stdlib net/http middleware handlers. There is nothing special about them, which means the router and all the tooling is designed to be compatible and friendly with any middleware in the community. This offers much better extensibility and reuse of packages and is at the heart of chi's purpose. + +## Basic Middleware Example +Here is an example of a standard net/http middleware where we assign a context key `"user"` the value of `"123"`. This middleware sets a hypothetical user identifier on the request context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + +We can now take these values from the context in our Handlers like this: +```go +func MyHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +## Usefull Pre-Made Middlewares + +We have some usefull middlewares in the package `github.com/go-chi/chi/middleware`. + +### Core middlewares +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + + +## Usefull Middleware Examples + +
+
+
CORS Middleware + +To Implement CORS in `chi` we can use [go-chi/cors](https://github.com/go-chi/cors) + +This middleware is designed to be used as a top-level middleware on the chi router. Applying with within a `r.Group()` or using `With()` **will not work without routes matching OPTIONS added**. + +#### Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +#### Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). + +
+
+
+ +
JWT Authentication Middleware + +To Implement JWT in `chi`, We can use [golang-jwt/jwt](https://github.com/golang-jwt/jwt) + +#### Usage +```go +// JSON returns a well formated response with a status code +func JSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.WriteHeader(statusCode) + err := json.NewEncoder(w).Encode(data) + if err != nil { + w.WriteHeader(500) + er := json.NewEncoder(w).Encode(map[string]interface{}{"error": "something unexpected occurred."}) + if er != nil { + return + } + } +} + +// Wrapping the Middleware in a function to access the secret key +func AuthJwtWrap(SecretKey string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var resp = map[string]interface{}{"error": "unauthorized", "message": "missing authorization token"} + var header = r.Header.Get("Authorization") + header = strings.TrimSpace(header) + if header == "" { + JSON(w, http.StatusUnauthorized, resp) + return + } + + token, err := jwt.Parse(header, func(token *jwt.Token) (interface{}, error) { + return []byte(SecretKey), nil + }) + + if err != nil { + resp["error"] = "unauthorized" + if err.Error() == "Token is expired" { + resp["message"] = err.Error() + JSON(w, http.StatusUnauthorized, resp) + return + } + resp["message"] = errorstring + JSON(w, http.StatusUnauthorized, resp) + log.Println(err.Error()) + return + } + + claims, _ := token.Claims.(jwt.MapClaims) + + uid, err := strconv.Atoi(claims["uid"].(string)) + if err != nil { + resp["error"] = "something unexpected occurred" + JSON(w, http.StatusInternalServerError, resp) + log.Println(err.Error()) + return + } + + ctx := context.WithValue(r.Context(), "uid", uid) // adding the user ID to the context + next.ServeHTTP(w, r.WithContext(ctx)) + + }) + } +} + +func main() { + r := chi.NewRouter() + + r.Use(AuthJwtWrap(secretKey)) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` +
+ +
+
+ +
Http Rate Limiting Middleware + +To implement this we can use [go-chi/httprate](https://github.com/go-chi/httprate) + +#### Usage +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/httprate" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + + // Enable httprate request limiter of 100 requests per minute. + // + // In the code example below, rate-limiting is bound to the request IP address + // via the LimitByIP middleware handler. + // + // To have a single rate-limiter for all requests, use httprate.LimitAll(..). + // + // Please see _example/main.go for other more, or read the library code. + r.Use(httprate.LimitByIP(100, 1*time.Minute)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(".")) + }) + + http.ListenAndServe(":3333", r) +} + +``` +
\ No newline at end of file diff --git a/docs/user_guide/routing.md b/docs/user_guide/routing.md new file mode 100644 index 00000000..8316d16d --- /dev/null +++ b/docs/user_guide/routing.md @@ -0,0 +1,163 @@ +# ๐Ÿ”Œ Routing + +> Routing refers to how an application's endpoints (URIs) respond to client requests. + +## Handling HTTP Request Methods + +`Chi` allows you to route/handle any HTTP request method, such as all the usual suspects: +GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT + +These methods are defined on the `chi.Router` as: + +```go +// HTTP-method routing along `pattern` +Connect(pattern string, h http.HandlerFunc) +Delete(pattern string, h http.HandlerFunc) +Get(pattern string, h http.HandlerFunc) +Head(pattern string, h http.HandlerFunc) +Options(pattern string, h http.HandlerFunc) +Patch(pattern string, h http.HandlerFunc) +Post(pattern string, h http.HandlerFunc) +Put(pattern string, h http.HandlerFunc) +Trace(pattern string, h http.HandlerFunc) +``` + +and may set a route by calling ie. `r.Put("/path", myHandler)`. + +You may also register your own custom method names, by calling `chi.RegisterMethod("JELLO")` +and then setting the routing handler via `r.Method("JELLO", "/path", myJelloMethodHandler)` + +## Routing patterns & url parameters + +Each routing method accepts a URL `pattern` and chain of `handlers`. + +The URL pattern supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). + +URL parameters can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters and `chi.URLParam(r, "*")` for a wildcard parameter. + +**Routing a slug:** + +```go +r := chi.NewRouter() + +r.Get("/articles/{date}-{slug}", getArticle) + +func getArticle(w http.ResponseWriter, r *http.Request) { + dateParam := chi.URLParam(r, "date") + slugParam := chi.URL(r, "slug") + article, err := database.GetArticle(date, slug) + + if err != nil { + w.WriteHeader(422) + w.Write([]byte(fmt.Sprintf("error fetching article %s-%s: %v", dateParam, slugParam, err))) + return + } + + if article == nil { + w.WriteHeader(404) + w.Write([]byte("article not found")) + return + } + w.Write([]byte(article.Text())) +}) +``` + +as you can see above, the url parameters are defined using the curly brackets `{}` with the parameter name in between, as `{date}` and `{slug}`. + +When a HTTP request is sent to the server and handled by the chi router, if the URL path matches the format of `/articles/{date}-{slug}`, then the `getArticle` function will be called to send a response to the client. + +For instance, URL paths like `/articles/20200109-this-is-so-cool` will match the route, however, +`/articles/1` will not. + +We can also use regex in url patterns + +For Example: +```go +r := chi.NewRouter() +r.Get("/articles/{rid:^[0-9]{5,6}}", getArticle) +``` + +## Making Custom 404 and 405 Handlers + +You can create Custom `http.StatusNotFound` and `http.StatusMethodNotAllowed` handlers in `chi` +```go +r.NotFound(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + w.Write([]byte("route does not exist")) +}) +r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write([]byte("method is not valid")) +}) +``` + +## Sub Routers + +You can create New Routers and Mount them on the Main Router to act as Sub Routers. + +For Example: +```go +func main(){ + r := chi.NewRouter() + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) + }) + + // Creating a New Router + apiRouter := chi.NewRouter() + apiRouter.Get("/articles/{date}-{slug}", getArticle) + + // Mounting the new Sub Router on the main router + r.Mount("/api", apiRouter) +} +``` + +Another Way of Implementing Sub Routers would be: +```go +r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) +``` + +## Routing Groups + +You can create Groups in Routers to segregate routes using a middleware and some not using a middleware + +for example: +```go +func main(){ + r := chi.NewRouter() + + // Public Routes + r.Group(func(r chi.Router) { + r.Get("/", HelloWorld) + r.Get("/{AssetUrl}", GetAsset) + r.Get("/manage/url/{path}", FetchAssetDetailsByURL) + r.Get("/manage/id/{path}", FetchAssetDetailsByID) + }) + + // Private Routes + // Require Authentication + r.Group(func(r chi.Router) { + r.Use(AuthMiddleware) + r.Post("/manage", CreateAsset) + }) + +} +``` + diff --git a/docs/user_guide/testing.md b/docs/user_guide/testing.md new file mode 100644 index 00000000..415aafc0 --- /dev/null +++ b/docs/user_guide/testing.md @@ -0,0 +1,116 @@ +# ๐Ÿงช Testing + +Writing tests for APIs is easy. We can use the inbuilt `net/http/httptest` lib to test our apis. + +### Usage +First we will create a simple Hello World Api +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func main() { + s := CreateNewServer() + s.MountHandlers() + http.ListenAndServe(":3000", s.Router) +} + +// HelloWorld api Handler +func HelloWorld(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) +} + +type Server struct { + Router *chi.Mux + // Db, config can be added here +} + +func CreateNewServer() *Server { + s := &Server{} + s.Router = chi.NewRouter() + return s +} + +func (s *Server) MountHandlers() { + // Mount all Middleware here + s.Router.Use(middleware.Logger) + + // Mount all handlers here + s.Router.Get("/", HelloWorld) + +} +``` +This is how a standard api would look, with a `Server` struct where we can add our router, and database connection...etc. + +We then write a `CreateNewServer` function to return a New Server with a `chi.Mux` Router + +We can then Mount all Handlers and middlewares in a single server method `MountHandlers` + + +We can now start writing tests for this. + +When writing tests, we will assert what values our api will return + +So for the route `/` our api should return `Hello World!` and a status code of `200` + + +Now in another file `main_test.go` +```go +package main + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// executeRequest, creates a new ResponseRecorder +// then executes the request by calling ServeHTTP in the router +// after which the handler writes the response to the response recorder +// which we can then inspect. +func executeRequest(req *http.Request, s *Server) *httptest.ResponseRecorder { + rr := httptest.NewRecorder() + s.Router.ServeHTTP(rr, req) + + return rr +} + +// checkResponseCode is a simple utility to check the response code +// of the response +func checkResponseCode(t *testing.T, expected, actual int) { + if expected != actual { + t.Errorf("Expected response code %d. Got %d\n", expected, actual) + } +} + +func TestHelloWorld(t *testing.T) { + // Create a New Server Struct + s := CreateNewServer() + // Mount Handlers + s.MountHandlers() + + // Create a New Request + req, _ := http.NewRequest("GET", "/", nil) + + // Execute Request + response := executeRequest(req, s) + + // Check the response code + checkResponseCode(t, http.StatusOK, response.Code) + + // We can use testify/require to assert values, as it is more convenient + require.Equal(t, "Hello World!", response.Body.String()) +} +``` + +Now run `go test ./... -v -cover`
+ +Voila, your tests work now. \ No newline at end of file