krsoninikhil / go-rest-kit

FastAPI like controllers for gin based Go apps. Ready made packages for quickly setting up REST APIs in Go
https://nikhilsoni.me/2024/05/27/fastapi-like-controllers-for-gin-based-go-apps/
MIT License
9 stars 1 forks source link
api golang rest tools

go-rest-kit

Ready made packages for quickly setting up REST APIs in Go. Specially avoiding to write handlers and request parsing for every new API you want. Limitation here is most packages are helful if you're using Gin for your APIs.

Motivation

Frameworks like FastAPI for Python are able to provide automatic request parsing and validation based on type hints and Python insn't even a typed language. I could not find any framework providing similar feature in Go, i.e. not having to repeat the same parsing and error handling in every API handler.

I should be able to write following handler / controller similar to FastAPI by just defining SpecificRequest and SpecificResponse i.e. get request body type as argument and return response type along with error:

func myHandler(ctx context.Context, request SpecificRequest) (*SpecificResponse, error) {}

Setup

This module is written with generic usecases in mind, which serves well in most usecase e.g. CRUD APIs. If you have a custom requirement, it's recommended to clone the repo in your development machine and use the local version of the module by replacing module path. This allows you extend and customize the types and methods provided as per your requirement.

go get github.com/krsoninikhil/go-rest-kit
git clone github.com/krsoninikhil/go-rest-kit ../
go mod edit -replace github.com/krsoninikhil/go-rest-kit=../go-rest-kit

Example

1. Exposing CRUD APIs

Exposing simple CRUD APIs is as simple as following

This example also shows use of request binding methods, and how you can avoid writing repeated code for parsing request body.

// models.go
type BusinessType struct {
    Name string
    Icon string
    pgdb.BaseModel
}

func (b BusinessType) ResourceName() string { return fmt.Sprintf("%T", b) }

// entities.go
type (
    BusinessTypeRequest struct {
        Name string `json:"name" binding:"required"`
        Icon string `json:"icon"`
    }
    BusinessTypeResponse struct {
        BusinessTypeRequest
        ID int `json:"id"`
    }
)

// implement `crud.Request`
func (b BusinessTypeRequest) ToModel(_ *gin.Context) BusinessType {
    return BusinessType{Name: b.Name, Icon: b.Icon}
}

// implement `crud.Response`
func (b BusinessTypeResponse) FillFromModel(m models.BusinessType) crud.Response[models.BusinessType] {
    return BusinessTypeResponse{
        ID: m.ID, 
        BusinessTypeRequest: BusinessTypeRequest{Name: m.Name, Icon: m.Icon},
    }
}
func (b BusinessTypeResponse) ItemID() int { return b.ID }

// setup routes
func main() {
    businessTypeDao        = crud.Dao[models.BusinessType]{PGDB: db} // use your own doa if you need custom implementation
    businessTypeCtlr = crud.Controller[models.BusinessType, types.BusinessTypeResponse, types.BusinessTypeRequest]{
        Svc: &businessTypeDao, // using dao for service as no business logic is required here
    } // prewritten controller struct with CRUD methods

    r := gin.Default()
    r.GET("/business-types", request.BindGet(businessTypeCtlr.Retrieve))
    r.GET("/business-types", request.BindGet(businessTypeCtlr.List))
    r.POST("/business-types", request.BindCreate(businessTypeCtlr.Create))
    r.PATCH("/business-types", request.BindUpdate(businessTypeCtlr.Update))
    r.DELETE("/business-types", request.BindDelete(businessTypeCtlr.Delete))
    // start your server
}

Best part here is -- you can replace any component (controller, usecase or dao) with your own implementation.

2. Load Your Application Config

Configs are loaded from yaml files where empty values are overriden from environment, which is set using .env file. e.g. if redis.password in your yaml is empty, it will be set by REDIS_PASSWORD env value. Neat, Hmm?

// define application config
type Config struct {
    DB pgdb.Config
    Twilio twilio.Config
    Auth auth.Config
    Env string
}
// implement `config.AppConfig`
func (c *Config) EnvPath() string    { return "./.env" } // path to your .env file
func (c *Config) SourcePath() string { return fmt.Sprintf("./api/%s.yml", c.Env) } // path to your yaml configuration file
func (c *Config) SetEnv(env string) { c.Env = env } // embed `config.BaseConfig` to avoid defining SetEnv

func main() {
    var (
        ctx = context.Background()
        conf Config
    )
    config.Load(ctx, &conf)
    // that's about it, you use the `conf` now, e.g. see below usage for creating postgres connection
    db := pgdb.NewPGConnection(ctx, conf.DB)
}

3. Exposing Auth APIs -- Signup, OTP Verification and Token Refresh

This also shows an example use of pgdb and twillio packages.

Assuming you have a models.User already defined, exposing auth APIs are just about defining your routes. Easy peazy!

func main() {
    // reusing gin engine `r`, configuration `conf` and postgres connection `db` from above examples
    var (
        cache          = cache.NewInMemory() // im memory cache impelementation is provided for the purpose of examples
        // injecting dependencies, use your own implementation for any object if default isn't enough for your usecase
        userDao        = auth.NewUserDao[models.User](db)
        authSvc        = auth.NewService(conf.Auth, userDao)
        smsProvider    = twilio.NewClient(conf.Auth.Twilio)
        otpSvc         = auth.NewOTPSvc(conf.Auth.OTP, smsProvider, cache) // 
        authController = auth.NewController(authSvc, otpSvc, cache)
    )

    r.POST("/auth/otp/send", request.BindCreate(authController.SendOTP))
    r.POST("/auth/otp/verify", request.BindCreate(authController.VerifyOTP))
    r.POST("/auth/token/refresh", request.BindCreate(authController.RefreshToken))
    r.GET("/countries/:alpha2Code", request.BindGet(authController.CountryInfo))
    // start your server
}

Getting Rid Of Repeated Code For Request Parsing In Every Handler

If you usecase is not a typical CRUD, don't worry, you can still use the generic binding from request package. While request.BindGet would parse only the query string for GET request, request.BindAll would parse values from request uri, body and query based on the tags defined in request struct.

func main() {
    r := gin.Default()
    r.GET("/business-types/insights", request.BindGet(businessTypeInsights))
    r.GET("/business-types/insights-2", request.BindAll(businessTypeInsights))
}

type (
    RequestType struct {}
    ResponseType struct {}
)

func businessTypeInsights(c *gin.Context, req RequestType) (*ResponseType, error) {
    // your customer controller
    return nil, apperrors.NewServerError(errors.New("not implemented"))
}

See full example for better understanding the implementation and usage here.

Packages Implementation Details