dimanche 2 décembre 2018

Design of lambda API written in Go

I need help with the design of my API that is written in Go. That's the file structure:

database/
 database.go
middlewares/
 authentication.go
models/
 pageview
services/
 pageviews/
   create/
     main.go
   show/
     main.go
   serverless.yml

database: Provides an Open function to get an instance of gorm.DB

middlewares: Augments the original request with properties that the request handler (or controller) needs to do its job

models: models definition and persistence logic

services: a service is like a project. It's where you define your AWS Lambda Functions, the events that trigger them and any AWS infrastructure resources they require, all in a file called serverless.yml.


By now I only have the pageviews service.

Let me show you what's inside the handler responsible for creating a pageview (services/pageviews/create/main.go):

package main

import (
    "context"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/clickhound/api/models"
)

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var pageviews models.Pageview
    if err := pageviews.Create(request.Body); err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    return events.APIGatewayProxyResponse{
        StatusCode: 201,
    }, nil
}

func main() {
    lambda.Start(Handler)
}

As you can see, the request handler (or controller) is responsible for delegating the creation of the resource to the model, let's see what it's inside the pageview model:

package models

import (
    "encoding/json"
)

type Pageview struct {
    ID       string
    Hostname string `gorm:"not null"`
}

func (p *Pageview) Create(data string) error {
    if err := json.Unmarshal([]byte(data), p); err != nil {
        return err
    }

    // TODO validate error here.
    db.Create(p)
    return nil
}

So, the model is responsible for:

  1. Unmarshal the request body
  2. Create the new resource

This starts to become messy when I need to return the data to the controller, let's say that I have a Find pageview. That's the request handler (or controller):

package main

import (
    "context"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/clickhound/api/middlewares"
    "github.com/clickhound/api/models"
)

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var pageview models.Pageview
    data, err := pageview.Find(request.PathParameters["id"])
    if err != nil {
        return events.APIGatewayProxyResponse{}, err
    }

    return events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body:       string(data),
    }, nil
}

func main() {
    lambda.Start(Handler))
}

And the models Find function:

func (p *Pageview) Find(id string) ([]byte, error) {
    p.ID = id

    // TODO validate error here.
    db.Find(p)
    return json.Marshal(p)
}

In this case, the model is responsible for:

  1. Find the resource
  2. Marshal the resource to JSON

As you can see, the model is responsible for both the persistence logic, but also return the response that the controller needs to do its job - I feel that something is misplaced, but why am I doing this?

I will introduce authentication, and some actions (like Find pageview) on the models should be limited to the current user. To achieve that, I will use an authentication middleware that injects the current user in the namespace of the models:

package middlewares

import (
    "context"

    "github.com/aws/aws-lambda-go/events"
    "github.com/clickhound/api/models"
)

func Authentication(next MiddlewareSignature) MiddlewareSignature {
    return func(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
        claims := request.RequestContext.Authorizer["claims"]

        if models.InjectUser(claims).RecordNotFound() {
            return events.APIGatewayProxyResponse{StatusCode: 401}, nil
        }

        return next(ctx, request)
    }
}

And in the user models:

package models

import (
    "time"

    "github.com/jinzhu/gorm"
    "github.com/mitchellh/mapstructure"
)


type User struct {
    ID        string `gorm:"not null"`
    Email     string `gorm:"not null;unique"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

func InjectUser(claims interface{}) *gorm.DB {
    if err := mapstructure.Decode(claims, user); err != nil {
        panic(err)
    }
    return db.Find(&user)
}

var user User

Now, any request handler (controller) that needs to do the operation limited to the current user, I can change:

func main() {
    lambda.Start(middlewares.Authentication(Handler))
}

to:

func main() {
    lambda.Start(
        middlewares.Authentication(Handler),
    )
}

Some questions:

  1. What do you think about injecting the user in the namespace of the models?
  2. What do you think about using the request handlers (controllers) to only call the proper function?
  3. What do you think about the models being responsible for the persistence logic, validating the database action, marshilling/unmarshalling the request/response data.

Thank you.

Aucun commentaire:

Enregistrer un commentaire