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
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:
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:
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:
It is good ideas to use some module to isolate business logic from transport details. It is just two different levels of abstraction and code became cleaner if we do not mix them. Although, it should be pragmatic and we can keep HTTP codes as they are universal language now and there is nothing wrong if your business logic returns 500 and 400 for different kinds of errors.
That separation would be main goal of controller if I write this code. Business logic layer (model) should work with strong type objects that model business domain and do not need to know about HTTP or AWS lambda implementation details.
Controller to handle:
Model: