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:
- Unmarshal the request body
- 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:
- Find the resource
- 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:
- What do you think about injecting the user in the namespace of the models?
- What do you think about using the request handlers (controllers) to only call the proper function?
- 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