This is a set of packages for creating REST & HTTP based microservices / backend servers in Go, with supporting functions and helpers. It's fairly opinionated and acts a little like a very minimal mini framework.
It purposefully doesn't use any web framework instead focusing on the base HTTP library and Chi for routing. Approaches such as composition which are idiomatic to Go, rather than classic dependency injection have been used.
The cmd
folder has an example of a working server/service which will accept REST requests and has a minimal API, serving as a reference.
The pkg
folder has a number of Go packages to support running REST APIs in Go, these are described in detail below
pkg/
├── api
├── auth
├── dapr/pubsub
├── env
├── httptester
├── problem
├── sse
└── static
This has been developed in Go 1.19+ and follows the https://github.com/golang-standards/project-layout guidelines for project structure. Which might be an acquired taste.
Also included are a standard and reusable Dockerfile & makefile, both of which inject version information into the build. The Makefile will also handle linting and running with hot-reload via air
This is a baseline from which you can extend, in order to run your own API, see the cmd/server.go
for an example of how this is done. A quick summary is:
import "github.com/benc-uk/go-rest-api/pkg/api"
type MyAPI struct {
// Embed and wrap the base API struct
*api.Base
// Add extra fields as per your implementation
foo Foo
}
router := chi.NewRouter()
api := MyAPI{
api.NewBase(serviceName, version, buildInfo, healthy),
}
api.AddHealthEndpoint(router, "health")
api.AddStatusEndpoint(router, "status")
The base API supports health checks and exposes data such as version and service name, plus helper functions for sending JSON & plain text responses or errors.
Optional endpoints which can be added to the API:
- Status endpoint, returning server & service details as JSON
- Prometheus metrics
- Health check
- Any routes you wish to return "200 OK" such as the root (/)
Optional middleware can be configured:
- Enabling permissive CORS policy (suggest you use chi/cors for finer grained control)
- Enriching HTTP request context with data extracted from JWT token. If a JWT token is found on any request, you can specify a claim, and the value of that claim will be extracted and put into the HTTP request context.
Supporting functions of the base API struct are, providing common API use cases:
StartServer(port int, router chi.Router, timeout time.Duration)
ReturnJSON(w http.ResponseWriter, data interface{})
ReturnText(w http.ResponseWriter, msg string)
ReturnErrorJSON(w http.ResponseWriter, err error)
ReturnOKJSON(w http.ResponseWriter)
This package contains Validator
interface which can be configured and used to enforce authentication on some or all routes of the API.
📝 Note: This package is generic and can be used with any code utilizing the net/http
library
There are two implementations of the Validator
interface:
PassthroughValidator
- Used when mocking & testing, or to conditionally switch auth offJWTValidator
- Main JWT based validator
The JWTValidator
takes three parameters when created:
- Client ID: An application client ID used when validating tokens, by checking the
aud
claim. - Scope: A scope string, validated against the
scp
claim. - JWKS URL: A URL of the keystore used to fetch public keys and and verify the signature of the token. This assumes tokens are signed with a public/private key algorithm e.g. RSA
It can be used two ways: router.Use(jwtValidator.Middleware)
to add validating middleware to all routes on a router. Alternatively jwtValidator.Protect(myHandler)
to wrap and protect certain handlers
Failed validation results in a HTTP 401 being returned.
Very basic set of helpers for fetching env vars with fallbacks to default values.
Provides support for RFC-7807 standard formatted responses to API errors. Use the Wrap()
function to wrap an error, and then Send()
to write it to the HTTP response writer.
// A rather contrived example
func (api MyAPI) getThing(resp http.ResponseWriter, req *http.Request) {
id := "some_id"
thing, err := api.dbContext.ExecuteSomeQuery(id, blah, blah)
// Return a RFC-7807 problem wrapping the database error, HTTP 500 will be sent
if err != nil {
problem.Wrap(500, req.RequestURI, "thing:"+id, err).Send(resp)
return
}
// Return a RFC-7807 problem describing the missing thing, HTTP 404 will be sent
if thing == nil {
problem.Wrap(404, req.RequestURI, "thing:"+id, errors.New("thing with that ID does not exist")).Send(resp)
return
}
// Handle success
}
Includes a SpaHandler
for serving SPA style static applications which may contain client routing logic. It acts as a wrapper around the standard http.FileServer
but rather than returning 404s it will return a fallback index file, e.g. index.html
Usage:
r := chi.NewRouter()
r.Handle("/*", static.SpaHandler{
StaticPath: "./", // Path to app content directory
IndexFile: "index.html", // Name of your SPA HTML file
})
srv := &http.Server{
Handler: r,
Addr: ":8080",
}
log.Fatal(srv.ListenAndServe())
Used to run through multiple test cases when integration testing an API or any HTTP service. Use the httptester.TestCase
struct and pass an array of them to httptester.Run()
Use to register your API with Dapr pub-sub and subscribe to a given topic and register a callback handler for messages received at that topic.
Provides FilteredRequestLogger
an extension of chi middleware logger which supports filtering out of requests from the logging output.
Provides support for Server Side Events. Two implementations are provided:
- Simple backend helper that can stream SSE events over HTTP
- A message broker which can be used to send messages to multiple clients, groups and keep track of connections/disconnections
Note. This package is standalone and will work with any Go HTTP implementation, you don't need to be using the api
or the other packages here.
Stream usage:
srv := sse.NewStreamer[string]()
// Send the time to the user every 1 second
go func() {
for {
timeNow := time.Now().Format("15:04:05")
srv.Messages <- "Hello it is now " + timeNow
time.Sleep(1 * time.Second)
}
}()
http.HandleFunc("/stream-time", func(w http.ResponseWriter, r *http.Request) {
srv.Stream(w, *r)
})
Broker usage:
srv := sse.NewBroker[string]()
// MessageAdapter is optional, but can format messages
srv.MessageAdapter = func(message string, clientID string) sse.SSE {
return sse.SSE{
Event: "message",
Data: "Some prefix " + message,
}
}
// Send a message every 1 second
go func() {
for {
srv.SendToAll("Hello! " + time.Now().String())
time.Sleep(1 * time.Second)
}
}()
http.HandleFunc("/stream-events", func(w http.ResponseWriter, r *http.Request) {
clientID := "You need to implement something here"
srv.Stream(clientID, w, *r)
})