Package apigo
is an drop-in adapter to AWS Lambda functions (based on go1.x
runtime) with a AWS API Gateway to easily reuse logic from serverfull http.Handler
s and provide the same experience for serverless function.
Add apigo
dependency using your vendor package manager (i.e. dep
) or go get
it:
go get -v github.com/piotrkubisa/apigo
If you have already registered some http.Handler
s, you can easily reuse them with apigo.Gateway
.
Example below illustrates how to create a hello world serverless application with apigo
:
package main
import (
"net/http"
"github.com/piotrkubisa/apigo"
)
func main() {
http.HandleFunc("/hello", helloHandler)
apigo.ListenAndServe("api.example.com", http.DefaultServeMux)
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`"Hello World"`))
}
If you have a bit more sophisticated deployment of your AWS Lambda functions then you probably would love to have more control over event-to-request transformation.
Imagine a situation if you have your API in one serverless function and you also have additional custom authorizer in separate AWS Lamda function.
In following scenario (presented in example below) context variable provided by serverless authorizer is passed to the API's http.Request
context, which can be further inspected during request handling:
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/aws/aws-lambda-go/events"
"github.com/go-chi/chi"
"github.com/piotrkubisa/apigo"
)
func main() {
g := &apigo.Gateway{
Proxy: &CustomProxy{apigo.DefaultProxy{"api.example.com"}},
Handler: routing(),
}
g.ListenAndServe()
}
type contextUsername struct{}
var keyUsername = &contextUsername{}
type CustomProxy struct {
apigo.DefaultProxy
}
func (p *CustomProxy) Transform(ctx context.Context, ev events.APIGatewayProxyRequest) (*http.Request, error) {
r, err := p.DefaultProxy.Transform(ctx, ev)
if err != nil {
return nil, err
}
// Add username to the http.Request's context from the custom authorizer
r = r.WithContext(
context.WithValue(
r.Context(),
keyUsername,
ev.RequestContext.Authorizer["username"],
),
)
return r, err
}
func routing() http.Handler {
r := chi.NewRouter()
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
log.Printf("id: %s", chi.URLParam(r, "id"))
// Remember: headers, status and then payload - always in this order
// set headers
w.Header().Set("Content-Type", "application/json")
// set status
w.WriteHeader(http.StatusOK)
// set response payload
username, _ := r.Context().Value(keyUsername).(string)
fmt.Fprintf(w, `"Hello %s"`, username)
})
return r
}
If you are going to use goroutines
in your AWS Lambda handler, then it is worth noting you should control its execution (i.e. by using sync.WaitGroup
), otherwise code in the goroutine
might be killed after returning a response to AWS API Gateway.
package main
import (
"net/http"
"sync"
"github.com/go-chi/chi"
"github.com/piotrkubisa/apigo"
)
func main() {
apigo.ListenAndServe("api.example.com", routing())
}
func routing() http.Handler {
r := chi.NewRouter()
r.Post("/cat", func(w http.ResponseWriter, r *http.Request) {
var wg sync.WaitGroup
wg.Add(2)
go sendIoTMessage(&wg)
go sendSlackNotification(&wg)
wg.Wait()
// Headers, status, payload
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`"Meow"`))
})
return r
}
func sendIoTMessage(wg *sync.WaitGroup) {
// ...
wg.Done()
}
func sendSlackNotification(wg *sync.WaitGroup) {
// ...
wg.Done()
}
Project has been forked from fabulous tj's apex/gateway repository, at 0bee09a.