Defend your API from the sieges. Bastion offers an "augmented" Router instance.
It has the minimal necessary to create an API with default handlers and middleware that help you raise your API easy and fast. Allows to have commons handlers and middleware between projects with the need for each one to do so. It's also included some useful/optional subpackages: middleware and render. We hope you enjoy it too!
go get -u github.com/ifreddyrondon/bastion
See _examples/ for a variety of examples.
As easy as:
package main
import (
"net/http"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/render"
)
func handler(w http.ResponseWriter, r *http.Request) {
render.JSON.Send(w, map[string]string{"message": "hello bastion"})
}
func main() {
app := bastion.New()
app.Get("/hello", handler)
// By default it serves on :8080 unless a
// ADDR environment variable was defined.
app.Serve()
// app.Serve(":3000") for a hard coded port
}
Bastion use go-chi as a router making it easy to modularize the applications.
Each Bastion instance 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.
NewRouter return a router as a subrouter along a routing path.
It's very useful to split up a large API as many independent routers and compose them as a single service.
package main
import (
"fmt"
"net/http"
"os"
"github.com/go-chi/chi"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/render"
)
// Routes creates a REST router for the todos resource
func routes() http.Handler {
r := bastion.NewRouter()
r.Get("/", list) // GET /todos - read a list of todos
r.Post("/", create) // POST /todos - create a new todo and persist it
r.Route("/{id}", func(r chi.Router) {
r.Get("/", get) // GET /todos/{id} - read a single todo by :id
r.Put("/", update) // PUT /todos/{id} - update a single todo by :id
r.Delete("/", delete) // DELETE /todos/{id} - delete a single todo by :id
})
return r
}
func list(w http.ResponseWriter, r *http.Request) {
render.Text.Response(w, http.StatusOK, "todos list of stuff..")
}
func create(w http.ResponseWriter, r *http.Request) {
render.Text.Response(w, http.StatusOK, "todos create")
}
func get(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
render.Text.Response(w, http.StatusOK, fmt.Sprintf("get todo with id %v", id))
}
func update(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
render.Text.Response(w, http.StatusOK, fmt.Sprintf("update todo with id %v", id))
}
func delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
render.Text.Response(w, http.StatusOK, fmt.Sprintf("delete todo with id %v", id))
}
func main() {
app := bastion.New()
app.Mount("/todo/", routes())
fmt.Fprintln(os.Stderr, app.Serve())
}
Bastion comes equipped with a set of commons middleware handlers, providing a suite of standard net/http
middleware.
They 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.
Name | Description |
---|---|
Logger | Logs the start and end of each request with the elapsed processing time. |
RequestID | Injects a request ID into the context of each request. |
Recovery | Gracefully absorb panics and prints the stack trace. |
InternalError | Intercept responses to verify if his status code is >= 500. If status is >= 500, it'll response with a default error. IT allows to response with the same error without disclosure internal information, also the real error is logged. |
Name | Description |
---|---|
Listing | Parses the url from a request and stores a listing.Listing on the context, it can be accessed through middleware.GetListing. |
WrapResponseWriter | provides an easy way to capture http related metrics from your application's http.Handlers or event hijack the response. |
Checkout for references, examples, options and docu in middleware or chi for more middlewares.
You can register a function to call on shutdown. This can be used to gracefully shutdown connections. By default the shutdown execute the server shutdown.
Bastion listens if any SIGINT, SIGTERM or SIGKILL signal is emitted and performs a graceful shutdown.
It can be added with RegisterOnShutdown
method of the bastion instance, it can accept variable number of functions.
package main
import (
"log"
"github.com/ifreddyrondon/bastion"
)
func onShutdown() {
log.Printf("My registered on shutdown. Doing something...")
}
func main() {
app := bastion.New()
app.RegisterOnShutdown(onShutdown)
app.Serve(":8080")
}
Options are used to define how the application should run, it can be set through optionals functions when using bastion.New()
.
package main
import (
"github.com/ifreddyrondon/bastion"
)
func main() {
// turn off pretty print logger and sets 500 errors message
bastion.New(bastion.DisablePrettyLogging(), bastion.InternalErrMsg(`Just another "500 - internal error"`))
}
Represent the message returned to the user when a http 500 error is caught by the InternalError middleware.
Default looks like something went wrong
.
InternalErrMsg(msg string)
set the message returned to the user when catch a 500 status error.
Boolean flag to disable the internal error middleware. Default false
.
DisableInternalErrorMiddleware()
turn off internal error middleware.
Boolean flag to disable recovery middleware. Default false
.
DisableRecoveryMiddleware()
turn off recovery middleware.
Boolean flag to disable the ping route. Default false
.
DisablePingRouter()
turn off ping route.
Boolean flag to disable the logger middleware. Default false
.
DisableLoggerMiddleware()
turn off logger middleware.
Boolean flag to don't output a colored human readable version on the out writer. Default false
.
DisablePrettyLogging()
turn off the pretty logging.
Defines log level. Default debug
. Allows for logging at the following levels (from highest to lowest):
-
panic, 5
-
fatal, 4
-
error, 3
-
warn, 2
-
info, 1
-
debug, 0
-
LoggerLevel(lvl string)
set the logger level.
package main
import (
"github.com/ifreddyrondon/bastion"
)
func main() {
bastion.New(bastion.LoggerLevel(bastion.ErrorLevel))
// or
bastion.New(bastion.LoggerLevel("error"))
}
Where the logger output write. Default os.Stdout
.
LoggerOutput(w io.Writer)
set the logger output writer.
Optional path prefix for profiler subrouter. If left unspecified, /debug/
is used as the default path prefix.
ProfilerRoutePrefix(prefix string)
set the prefix path for the profile router.
Boolean flag to enable the profiler subrouter in production mode.
EnableProfiler()
turn on profiler subrouter.
Mode in which the App is running. Default is "debug".
Can be set using Mode(string)
option or with ENV vars GO_ENV
or GO_ENVIRONMENT
. Mode(mode string)
has more priority
than the ENV variables.
When production mode is on, the request logger IP, UserAgent and Referer are enable, the logger level is set
to error
(is not set with LoggerLevel option), the profiler routes are disable (is not set with EnableProfiler option)
and the logging pretty print is disabled.
Mode(mode string)
set the mode in which the App is running.
package main
import (
"github.com/ifreddyrondon/bastion"
)
func main() {
bastion.New(bastion.Mode(bastion.DebugMode))
// or
bastion.New(bastion.Mode("production"))
}
Bastion comes with battery included testing tools to perform End-to-end test over your endpoint/handlers.
It uses github.com/gavv/httpexpect to incrementally build HTTP requests, inspect HTTP responses and inspect response payload recursively.
- Create the bastion instance with the handler you want to test.
- Import from
bastion.Tester
- It receive a
*testing.T
and*bastion.Bastion
instances as params. - Build http request.
- Inspect http response.
- Inspect response payload.
package main_test
import (
"net/http"
"testing"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/_examples/todo-rest/todo"
"github.com/ifreddyrondon/bastion/render"
)
func setup() *bastion.Bastion {
app := bastion.New()
app.Mount("/todo/", todo.Routes())
return app
}
func TestHandlerCreate(t *testing.T) {
app := setup()
payload := map[string]interface{}{
"description": "new description",
}
e := bastion.Tester(t, app)
e.POST("/todo/").WithJSON(payload).Expect().
Status(http.StatusCreated).
JSON().Object().
ContainsKey("id").ValueEqual("id", 0).
ContainsKey("description").ValueEqual("description", "new description")
}
Go and check the full test for handler and complete app 🤓
Easily rendering JSON, XML, binary data, and HTML templates responses
It can be used with pretty much any web framework providing you can access the http.ResponseWriter
from your handler.
The rendering functions simply wraps Go's existing functionality for marshaling and rendering data.
package main
import (
"encoding/xml"
"net/http"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/render"
)
type ExampleXML struct {
XMLName xml.Name `xml:"example"`
One string `xml:"one,attr"`
Two string `xml:"two,attr"`
}
func main() {
app := bastion.New()
app.Get("/data", func(w http.ResponseWriter, req *http.Request) {
render.Data.Response(w, http.StatusOK, []byte("Some binary data here."))
})
app.Get("/text", func(w http.ResponseWriter, req *http.Request) {
render.Text.Response(w, http.StatusOK, "Plain text here")
})
app.Get("/html", func(w http.ResponseWriter, req *http.Request) {
render.HTML.Response(w, http.StatusOK, "<h1>Hello World</h1>")
})
app.Get("/json", func(w http.ResponseWriter, req *http.Request) {
render.JSON.Response(w, http.StatusOK, map[string]string{"hello": "json"})
})
app.Get("/json-ok", func(w http.ResponseWriter, req *http.Request) {
// with implicit status 200
render.JSON.Send(w, map[string]string{"hello": "json"})
})
app.Get("/xml", func(w http.ResponseWriter, req *http.Request) {
render.XML.Response(w, http.StatusOK, ExampleXML{One: "hello", Two: "xml"})
})
app.Get("/xml-ok", func(w http.ResponseWriter, req *http.Request) {
// with implicit status 200
render.XML.Send(w, ExampleXML{One: "hello", Two: "xml"})
})
app.Serve()
}
Checkout more references, examples, options and implementations in render.
To bind a request body or a source input into a type, use a binder. It's currently support binding of JSON, XML and YAML.
The binding execute Validate()
if the type implements the binder.Validate
interface after successfully bind the type.
The goal of implement Validate
is to endorse the values linked to the type. This library intends for you to handle
your own validations error.
package main
import (
"net/http"
"github.com/pkg/errors"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/binder"
"github.com/ifreddyrondon/bastion/render"
)
type address struct {
Address *string `json:"address" xml:"address" yaml:"address"`
Lat float64 `json:"lat" xml:"lat" yaml:"lat"`
Lng float64 `json:"lng" xml:"lng" yaml:"lng"`
}
func (a *address) Validate() error {
if a.Address == nil || *a.Address == "" {
return errors.New("missing address field")
}
return nil
}
func main() {
app := bastion.New()
app.Post("/decode-json", func(w http.ResponseWriter, r *http.Request) {
var a address
if err := binder.JSON.FromReq(r, &a); err != nil {
render.JSON.BadRequest(w, err)
return
}
render.JSON.Send(w, a)
})
app.Post("/decode-xml", func(w http.ResponseWriter, r *http.Request) {
var a address
if err := binder.XML.FromReq(r, &a); err != nil {
render.JSON.BadRequest(w, err)
return
}
render.JSON.Send(w, a)
})
app.Post("/decode-yaml", func(w http.ResponseWriter, r *http.Request) {
var a address
if err := binder.YAML.FromReq(r, &a); err != nil {
render.JSON.BadRequest(w, err)
return
}
render.JSON.Send(w, a)
})
app.Serve()
}
Checkout more references, examples, options and implementations in binder.
Bastion have an internal JSON structured logger powered by github.com/rs/zerolog.
It can be accessed from the context of each request l := bastion.LoggerFromCtx(ctx)
. The request id is logged for
every call to the logger.
package main
import (
"net/http"
"github.com/ifreddyrondon/bastion"
"github.com/ifreddyrondon/bastion/render"
)
func handler(w http.ResponseWriter, r *http.Request) {
l := bastion.LoggerFromCtx(r.Context())
l.Info().Msg("handler")
render.JSON.Send(w, map[string]string{"message": "hello bastion"})
}
func main() {
app := bastion.New()
app.Get("/hello", handler)
app.Serve()
}