Skip to content
Drop-in adapter for AWS Lambda & API Gateway to reuse http.Handler in serveless functions
Go
Branch: master
Clone or download

README.md

apigo

Documentation Build Status Go Report Card

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.Handlers and provide the same experience for serverless function.

Installation

Add apigo dependency using your vendor package manager (i.e. dep) or go get it:

go get -v github.com/piotrkubisa/apigo

Usage

Default behaviour

If you have already registered some http.Handlers, 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"`))
}

Custom event-to-request transformation

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
}

Goroutines

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()
}

Credits

Project has been forked from fabulous tj's apex/gateway repository, at 0bee09a.

You can’t perform that action at this time.