Documentation
¶
Overview ¶
Package lit is a fast and expressive HTTP framework.
The basics ¶
In Lit, an HTTP handler is a function that receives a *Request and returns a Response. Register new handlers using the *Router.Handle method.
For instance, the Divide function below is a handler that returns the division of two integers coming from query parameters:
type Request struct { A int `query:"a"` B int `query:"b"` } func (r *Request) Validate() []validate.Field { return []validate.Field{ validate.NotEqual(&r.B, 0), } } func Divide(r *lit.Request) lit.Response { req, err := bind.Query[Request](r) if err != nil { return render.BadRequest(err) } return render.OK(req.A / req.B) }
In order to extend a handler's functionality and to be able to reuse the logic in several handlers, such as logging or authorization logic, one can use middlewares. In Lit, a middleware is a function that receives a Handler and returns a Handler. Register new middlewares using the *Router.Use method.
For instance, the AppendRequestID function below is a middleware that assigns an ID to the request and appends it to the context:
type ContextKeyType string var RequestIDKey ContextKeyType = "request-id" func AppendRequestID(h lit.Handler) lit.Handler { return func(r *lit.Request) lit.Response { var ( requestID = uuid.New() ctx = context.WithValue(r.Context(), RequestIDKey, requestID) ) r.WithContext(ctx) return h(r) } }
It is recommended to use the Log and Recover middlewares.
Check the package-level examples for more use cases.
Model binding and receiving files ¶
Lit can parse and validate data coming from a request's URI parameters, header, body or query parameters to Go structs, including files from multipart form requests.
Check github.com/jvcoutinho/lit/bind package.
Validation ¶
Lit can validate Go structs with generics and compile-time assertions.
Check github.com/jvcoutinho/lit/validate package.
Responding requests, redirecting, serving files and streams ¶
Lit responds requests with implementations of the Response interface. Current provided implementations include JSON responses, redirections, no content responses, files and streams.
Check github.com/jvcoutinho/lit/render package.
Testing handlers ¶
Handlers can be unit tested in several ways. The simplest and idiomatic form is calling the handler with a crafted request and asserting the response:
type Request struct { A int `query:"a"` B int `query:"b"` } func (r *Request) Validate() []validate.Field { return []validate.Field{ validate.NotEqual(&r.B, 0), } } func Divide(r *lit.Request) lit.Response { req, err := bind.Query[Request](r) if err != nil { return render.BadRequest(err) } return render.OK(req.A / req.B) } func TestDivide(t *testing.T) { t.Parallel() tests := []struct { description string a int b int want lit.Response }{ { description: "BEquals0", a: 3, b: 0, want: render.BadRequest("b should not be equal to 0"), }, { description: "Division", a: 6, b: 3, want: render.OK(2), }, } for _, test := range tests { test := test t.Run(test.description, func(t *testing.T) { t.Parallel() var ( path = fmt.Sprintf("/?a=%d&b=%d", test.a, test.b) request = lit.NewRequest( httptest.NewRequest(http.MethodGet, path, nil), ) got = Divide(request) want = test.want ) if !reflect.DeepEqual(got, want) { t.Fatalf("got: %v; want: %v", got, want) } }) } }
Testing middlewares ¶
Middlewares can be tested in the same way as handlers (crafting a request and asserting the response of the handler after the transformation):
func ValidateXAPIKeyHeader(h lit.Handler) lit.Handler { return func(r *lit.Request) lit.Response { apiKeyHeader, err := bind.HeaderField[string](r, "X-API-KEY") if err != nil { return render.BadRequest(err) } if apiKeyHeader == "" { return render.Unauthorized("API Key must be provided") } return h(r) } } func TestValidateXAPIKeyHeader(t *testing.T) { t.Parallel() testHandler := func(r *lit.Request) lit.Response { return render.NoContent() } tests := []struct { description string apiKeyHeader string want lit.Response }{ { description: "EmptyHeader", apiKeyHeader: "", want: render.Unauthorized("API Key must be provided"), }, { description: "ValidAPIKey", apiKeyHeader: "api-key-1", want: render.NoContent(), }, } for _, test := range tests { test := test t.Run(test.description, func(t *testing.T) { t.Parallel() r := httptest.NewRequest(http.MethodGet, "/", nil) r.Header.Add("X-API-KEY", test.apiKeyHeader) var ( request = lit.NewRequest(r) got = ValidateXAPIKeyHeader(testHandler)(request) ) if !reflect.DeepEqual(got, test.want) { t.Fatalf("got: %v; want: %v", got, test.want) } }) } }
Example (AuthorizationMiddlewares) ¶
package main import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "github.com/jvcoutinho/lit" "github.com/jvcoutinho/lit/bind" "github.com/jvcoutinho/lit/render" "github.com/jvcoutinho/lit/validate" ) // ValidateXAPIKeyHeader validates if the X-API-Key header is not empty. func ValidateXAPIKeyHeader(h lit.Handler) lit.Handler { return func(r *lit.Request) lit.Response { apiKeyHeader, err := bind.HeaderField[string](r, "X-API-KEY") if err != nil { return render.BadRequest(err) } if apiKeyHeader == "" { return render.Unauthorized("X-API-Key must be provided") } fmt.Printf("Authorized request for %s\n", apiKeyHeader) return h(r) } } type GetUserNameRequest struct { UserID string `uri:"user_id"` } func (r *GetUserNameRequest) Validate() []validate.Field { return []validate.Field{ validate.UUID(&r.UserID), } } type GetUserNameResponse struct { Name string `json:"name"` } // GetUserName gets the name of an identified user. func GetUserName(r *lit.Request) lit.Response { _, err := bind.URIParameters[GetUserNameRequest](r) if err != nil { return render.BadRequest(err) } // getting user name... res := GetUserNameResponse{"John"} return render.OK(res) } type PatchUserNameRequest struct { UserID string `uri:"user_id"` Name string `json:"name"` } func (r *PatchUserNameRequest) Validate() []validate.Field { return []validate.Field{ validate.UUID(&r.UserID), validate.MinLength(&r.Name, 3), } } // PatchUserName patches the name of an identified user. func PatchUserName(r *lit.Request) lit.Response { _, err := bind.Request[PatchUserNameRequest](r) if err != nil { return render.BadRequest(err) } // patching user name... return render.NoContent() } func main() { r := lit.NewRouter() r.GET("/users/:user_id/name", GetUserName) r.PATCH("/users/:user_id/name", PatchUserName, ValidateXAPIKeyHeader) requestAPIKey(r, http.MethodGet, "/users/19fb2f66-f335-47ef-a1ca-1d02d1a117c8/name", "", nil) requestAPIKey(r, http.MethodPatch, "/users/19fb2f66-f335-47ef-a1ca-1d02d1a117c8/name", "", strings.NewReader(`{"name":"John"}`)) requestAPIKey(r, http.MethodPatch, "/users/19fb2f66-f335-47ef-a1ca-1d02d1a117c8/name", "api-key-1", strings.NewReader(`{"name":"John"}`)) } func requestAPIKey(r *lit.Router, method, path, header string, body io.Reader) { res := httptest.NewRecorder() req := httptest.NewRequest(method, path, body) req.Header.Add("X-API-KEY", header) r.ServeHTTP(res, req) fmt.Println(res.Body, res.Code) }
Output: {"name":"John"} 200 {"message":"X-API-Key must be provided"} 401 Authorized request for api-key-1 204
Example (CalculatorAPI) ¶
package main import ( "fmt" "net/http" "net/http/httptest" "github.com/jvcoutinho/lit" "github.com/jvcoutinho/lit/bind" "github.com/jvcoutinho/lit/render" "github.com/jvcoutinho/lit/validate" ) type BinaryOperationRequest struct { A int `query:"a"` B int `query:"b"` } // Add returns the sum of two integers. func Add(r *lit.Request) lit.Response { req, err := bind.Query[BinaryOperationRequest](r) if err != nil { return render.BadRequest(err) } return render.OK(req.A + req.B) } // Subtract returns the subtraction of two integers. func Subtract(r *lit.Request) lit.Response { req, err := bind.Query[BinaryOperationRequest](r) if err != nil { return render.BadRequest(err) } return render.OK(req.A - req.B) } // Multiply returns the multiplication of two integers. func Multiply(r *lit.Request) lit.Response { req, err := bind.Query[BinaryOperationRequest](r) if err != nil { return render.BadRequest(err) } return render.OK(req.A * req.B) } // Divide returns the division of two integers, granted the divisor is different from zero. func Divide(r *lit.Request) lit.Response { req, err := bind.Query[BinaryOperationRequest](r) if err != nil { return render.BadRequest(err) } if err := validate.Fields(&req, validate.NotEqual(&req.B, 0), ); err != nil { return render.BadRequest(err) } return render.OK(req.A / req.B) } func main() { r := lit.NewRouter() r.GET("/add", Add) r.GET("/sub", Subtract) r.GET("/mul", Multiply) r.GET("/div", Divide) request(r, "/add?a=2&b=3") request(r, "/sub?a=3&b=3") request(r, "/mul?a=3&b=9") request(r, "/div?a=6&b=2") request(r, "/div?a=6&b=0") request(r, "/add?a=2a&b=3") } func request(r *lit.Router, path string) { res := httptest.NewRecorder() r.ServeHTTP( res, httptest.NewRequest(http.MethodGet, path, nil), ) fmt.Println(res.Body) }
Output: 5 0 27 3 {"message":"b should not be equal to 0"} {"message":"a: 2a is not a valid int: invalid syntax"}
Example (CorsHandler) ¶
package main import ( "fmt" "net/http" "net/http/httptest" "github.com/jvcoutinho/lit" "github.com/jvcoutinho/lit/render" ) // EnableCORS handles a preflight CORS OPTIONS request. func EnableCORS(_ *lit.Request) lit.Response { res := render.NoContent() return lit.ResponseFunc(func(w http.ResponseWriter) { res.Write(w) header := w.Header() header.Set("Access-Control-Allow-Origin", "*") header.Set("Access-Control-Allow-Credentials", "false") header.Set("Access-Control-Allow-Methods", header.Get("Allow")) }) } func HelloWorld(_ *lit.Request) lit.Response { return render.OK("Hello, World!") } func main() { r := lit.NewRouter() r.HandleOPTIONS(EnableCORS) r.GET("/", HelloWorld) req := httptest.NewRequest(http.MethodOptions, "/", nil) res := httptest.NewRecorder() r.ServeHTTP(res, req) fmt.Println(res.Header(), res.Code) }
Output: map[Access-Control-Allow-Credentials:[false] Access-Control-Allow-Methods:[GET, OPTIONS] Access-Control-Allow-Origin:[*] Allow:[GET, OPTIONS]] 204
Example (CustomMethodNotAllowedHandler) ¶
package main import ( "fmt" "net/http" "net/http/httptest" "github.com/jvcoutinho/lit" "github.com/jvcoutinho/lit/render" ) func MethodNotAllowedHandler(_ *lit.Request) lit.Response { return render.JSON(http.StatusMethodNotAllowed, "Unsupported method") } func main() { r := lit.NewRouter() r.HandleMethodNotAllowed(MethodNotAllowedHandler) r.GET("/", HelloWorld) req := httptest.NewRequest(http.MethodPost, "/", nil) res := httptest.NewRecorder() r.ServeHTTP(res, req) fmt.Println(res.Body, res.Code) }
Output: {"message":"Unsupported method"} 405
Example (CustomNotFoundHandler) ¶
package main import ( "fmt" "net/http" "net/http/httptest" "github.com/jvcoutinho/lit" "github.com/jvcoutinho/lit/render" ) func NotFoundHandler(_ *lit.Request) lit.Response { return render.NotFound("Not found. Try again later.") } func main() { r := lit.NewRouter() r.HandleNotFound(NotFoundHandler) req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() r.ServeHTTP(res, req) fmt.Println(res.Body, res.Code) }
Output: {"message":"Not found. Try again later."} 404
Index ¶
- type Handler
- type Middleware
- type Recorder
- type Request
- func (r *Request) Base() *http.Request
- func (r *Request) Body() io.ReadCloser
- func (r *Request) Context() context.Context
- func (r *Request) Header() http.Header
- func (r *Request) Method() string
- func (r *Request) URIParameters() map[string]string
- func (r *Request) URL() *url.URL
- func (r *Request) WithContext(ctx context.Context) *Request
- func (r *Request) WithRequest(req *http.Request) *Request
- func (r *Request) WithURIParameters(parameters map[string]string) *Request
- type Response
- type ResponseFunc
- type Router
- func (r *Router) DELETE(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) GET(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) HEAD(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) Handle(path string, method string, handler Handler, middlewares ...Middleware)
- func (r *Router) HandleMethodNotAllowed(handler Handler)
- func (r *Router) HandleNotFound(handler Handler)
- func (r *Router) HandleOPTIONS(handler Handler)
- func (r *Router) OPTIONS(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) PATCH(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) POST(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) PUT(path string, handler Handler, middlewares ...Middleware)
- func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request)
- func (r *Router) Use(m Middleware)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Handler ¶
Handler handles requests.
func Log ¶
Log is a simple middleware that logs information data about the request:
- The method and path of the request;
- The status code of the response;
- The time of the request;
- The client's remote address;
- The duration of the request;
- The content length of the response body.
func Recover ¶
Recover is a simple middleware that recovers if h panics, responding a 500 Internal Server Error with the panic value as the body and logging the stack trace in os.Stderr.
func (Handler) Base ¶
func (h Handler) Base() http.HandlerFunc
Base returns the equivalent http.HandlerFunc of this handler.
type Middleware ¶
Middleware transforms a Handler, extending its functionality.
type Recorder ¶
type Recorder struct { // StatusCode of this response. StatusCode int // Size of this response. ContentLength int http.ResponseWriter http.Hijacker http.Flusher // contains filtered or unexported fields }
Recorder is a http.ResponseWriter that keeps track of the response' status code and content length.
func NewRecorder ¶
func NewRecorder(w http.ResponseWriter) *Recorder
NewRecorder creates a new *Recorder instance from a http.ResponseWriter.
func (*Recorder) WriteHeader ¶
type Request ¶
type Request struct {
// contains filtered or unexported fields
}
Request is the input of a Handler.
func NewEmptyRequest ¶ added in v0.1.1
func NewEmptyRequest() *Request
NewEmptyRequest creates a new Request instance.
func NewRequest ¶
NewRequest creates a new Request instance from a *http.Request.
If request is nil, NewRequest panics.
func (*Request) Base ¶
Base returns the equivalent *http.Request of this request.
func (*Request) URIParameters ¶
URIParameters returns this request's URL path parameters and their values. It can be nil, meaning the handler expects no parameters.
Use [bind.URIParameters] for standard model binding and validation features.
The keys from this map don't start with the ":" prefix.
func (*Request) WithContext ¶
WithContext sets the context of this request.
If ctx is nil, WithContext panics.
func (*Request) WithRequest ¶ added in v0.1.1
WithRequest sets the base request of this request.
If req is nil, WithRequest panics.
type Response ¶
type Response interface { // Write responds the request. Write(w http.ResponseWriter) }
Response is the output of a Handler.
type ResponseFunc ¶
type ResponseFunc func(w http.ResponseWriter)
ResponseFunc writes response data into http.ResponseWriter.
func (ResponseFunc) Write ¶
func (r ResponseFunc) Write(w http.ResponseWriter)
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router manages, listens and serves HTTP requests.
func (*Router) DELETE ¶
func (r *Router) DELETE(path string, handler Handler, middlewares ...Middleware)
DELETE registers handler for path and DELETE method and optional local middlewares.
It's equivalent to:
Handle(path, "DELETE", handler, middlewares)
func (*Router) GET ¶
func (r *Router) GET(path string, handler Handler, middlewares ...Middleware)
GET registers handler for path and GET method and optional local middlewares.
It's equivalent to:
Handle(path, "GET", handler, middlewares)
func (*Router) HEAD ¶
func (r *Router) HEAD(path string, handler Handler, middlewares ...Middleware)
HEAD registers handler for path and HEAD method and optional local middlewares.
It's equivalent to:
Handle(path, "HEAD", handler, middlewares)
func (*Router) Handle ¶
func (r *Router) Handle(path string, method string, handler Handler, middlewares ...Middleware)
Handle registers handler for path and method and optional local middlewares.
Middlewares transform handler. They are applied in reverse order, and local middlewares are always applied first. For example, suppose there have been defined global middlewares G1 and G2 in this order and local middlewares L1 and L2 in this order. The response for the request r is
(G1(G2(L1(L2(handler)))))(r)
If path does not contain a leading slash, method is empty, handler is nil or a middleware is nil, Handle panics.
func (*Router) HandleMethodNotAllowed ¶
HandleMethodNotAllowed registers handler to be called when there is a match to a route, but not with that method. By default, Lit uses a wrapped http.Error with status code 405 Method Not Allowed.
If handler is nil, HandleMethodNotAllowed clears the current set handler. In this case, the behaviour is to call the Not Found handler (either the one defined in HandleNotFound or the default one).
func (*Router) HandleNotFound ¶
HandleNotFound registers handler to be called when no matching route is found. By default, Lit uses a wrapped http.NotFound.
If handler is nil, HandleNotFound panics.
func (*Router) HandleOPTIONS ¶ added in v0.1.4
HandleOPTIONS registers handler to be called when the request method is OPTIONS. By default, Lit sets the Allow header with supported methods.
Useful to support preflight CORS requests, for instance.
If handler is nil, HandleOPTIONS clears the current set handler. In this case, the behaviour is to call the registered handler normally, if there is one.
func (*Router) OPTIONS ¶
func (r *Router) OPTIONS(path string, handler Handler, middlewares ...Middleware)
OPTIONS registers handler for path and OPTIONS method and optional local middlewares.
It's equivalent to:
Handle(path, "OPTIONS", handler, middlewares)
func (*Router) PATCH ¶
func (r *Router) PATCH(path string, handler Handler, middlewares ...Middleware)
PATCH registers handler for path and PATCH method and optional local middlewares.
It's equivalent to:
Handle(path, "PATCH", handler, middlewares)
func (*Router) POST ¶
func (r *Router) POST(path string, handler Handler, middlewares ...Middleware)
POST registers handler for path and POST method and optional local middlewares.
It's equivalent to:
Handle(path, "POST", handler, middlewares)
func (*Router) PUT ¶
func (r *Router) PUT(path string, handler Handler, middlewares ...Middleware)
PUT registers handler for path and PUT method and optional local middlewares.
It's equivalent to:
Handle(path, "PUT", handler, middlewares)
func (*Router) ServeHTTP ¶
func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request)
ServeHTTP dispatches the request to the handler whose pattern most closely matches the request URL and whose method is the same as the request method.
func (*Router) Use ¶
func (r *Router) Use(m Middleware)
Use registers m as a global middleware. They run in every request.
Middlewares transform the request handler. They are applied in reverse order, and local middlewares are always applied first. For example, suppose there have been defined global middlewares G1 and G2 in this order and local middlewares L1 and L2 in this order. The response for the request r handled by h is
(G1(G2(L1(L2(h)))))(r)
Global middlewares should be set before any handler is registered.
If m is nil, Use panics.
Directories
¶
Path | Synopsis |
---|---|
Package bind contains model binding features to be used along [*lit.Request].
|
Package bind contains model binding features to be used along [*lit.Request]. |
Package render contains implementations of [lit.Response], suitable for responding requests.
|
Package render contains implementations of [lit.Response], suitable for responding requests. |
Package validate contains field validations for Go structs, appropriated to use with [*lit.Request].
|
Package validate contains field validations for Go structs, appropriated to use with [*lit.Request]. |