A different approach to Go web frameworks

Overview

gongular

Go Report Card GoDoc

Note: gongular recently updated, and if you are looking for the previous version it is tagged as v.1.0

gongular is an HTTP Server Framework for developing APIs easily. It is like Gin Gonic, but it features Angular-like (or Spring like) dependency injection and better input handling. Most of the time, user input must be transformed into a structured data then it must be validated. It takes too much time and is a repetitive work, gongular aims to reduce that complexity by providing request-input mapping with tag based validation.

Note: gongular is an opinionated framework and it heavily relies on reflection to achieve these functionality. While there are tests to ensure it works flawlessly, I am open to contributions and opinions on how to make it better.

Features

  • Automatic Query, POST Body, URL Param binding to structs with easy validation
  • Easy and simple dependency injection i.e passing DB connections and other values
  • Custom dependency injection with user specified logic, i.e as User struct from a session
  • Route grouping that allows reducing duplicated code
  • Middlewares that can do preliminary work before routes, groups which might be helpful for authentication checks, logging etc.
  • Static file serving
  • Very fast thanks to httprouter

Simple Usage

gongular aims to be simple as much as possible while providing flexibility. The below example is enough to reply user with its IP.

type WelcomeMessage struct {}
func(w *WelcomeMessage) Handle(c *gongular.Context) error {
    c.SetBody(c.Request().RemoteAddr)
}

g := gongular.NewEngine()
g.GET("/", &WelcomeMessage{})
g.ListenAndServe(":8000")

How to Use

All HTTP handlers in gongular are structs with Handle(c *gongular.Context) error function or in other words RequestHandler interface, implemented. Request handler objects are flexible. They can have various fields, where some of the fields with specific names are special. For instance, if you want to bind the path parameters, your handler object must have field named Param which is a flat struct. Also you can have a Query field which also maps to query parameters. Body field lets you map to JSON body, and Form field lets you bind into form submissions with files.

type MyHandler struct {
    Param struct {
        UserID int       
    }
    Query struct {
        Name  string
        Age   int
        Level float64
    }
    Body struct {
        Comment string
        Choices []string
        Address struct {
            City    string
            Country string
            Hello   string            
        }
    }
}
func(m *MyHandler) Handle(c *gongular.Context) error {
    c.SetBody("Wow so much params")
    return nil
}

Path Parameters

We use julienschmidt/httprouter to multiplex requests and do parametric binding to requests. So the format :VariableName, *somepath is supported in paths. Note that, you can use valid struct tag to validate parameters.

type PathParamHandler struct {
    Param struct {
        Username string
    }
}
func(p *PathParamHandler) Handle(c *Context) error {
    c.SetBody(p.Param.Username)
    return nil
}

Query Parameters

Query parameter is very similar to path parameters, the only difference the field name should be Query and it should also be a flat struct with no inner parameters or arrays. Query params are case sensitive and use the exact name of the struct property by default. You can use the q struct tag to specify the parameter key

type QueryParamHandler struct {
    Query struct {
        Username string `q:"username"`
        Age int
    }
}
func(p *QueryParamHandler) Handle(c *Context) error {
    println(p.Param.Age)
    c.SetBody(p.Param.Username)
    return nil
}

JSON Request Body

JSON request bodies can be parsed similar to query parameters, but JSON body can be an arbitrary struct.

type BodyParamHandler struct {
    Body struct {
        Username string
        Age int
        Preferences []string
        Comments []struct {
        	OwnerID int
        	Message string
        }
    }
}
func(p *BodyParamHandler) Handle(c *Context) error {
    println(p.Body.Age)
    c.SetBody(p.Body.Preferences + len(c.Body.Comments))
    return nil
}

Forms and File Uploading

Please note that Body and Form cannot be both present in the same handler, since the gongular would confuse what to do with the request body.

type formHandler struct {
	Form struct {
		Age      int
		Name     string
		Favorite string
		Fraction float64
	}
}

func (q *formHandler) Handle(c *Context) error {
	c.SetBody(fmt.Sprintf("%d:%s:%s:%.2f",
		q.Form.Age, q.Form.Name, q.Form.Favorite, q.Form.Fraction))
	return nil
}

e.GetRouter().POST("/submit", &formHandler{})

File Uploading

For uploaded files, we use a special struct to hold them in the Form value of the request struct. UploadedFile holds the multipart.File and the multipart.Header, you can do anything you want with them.

type UploadedFile struct {
	File   multipart.File
	Header *multipart.FileHeader
}

You can use it in the handler like the following:

type formUploadTest struct {
	Form struct {
		SomeFile     *UploadedFile
		RegularValue int
	}
}

func (f *formUploadTest) Handle(c *Context) error {
	s := sha256.New()
	io.Copy(s, f.Form.SomeFile.File)
	resp := fmt.Sprintf("%x:%d", s.Sum(nil), f.Form.RegularValue)
	c.SetBody(resp)
	return nil
}

e.GetRouter().POST("/upload", &formUploadTest{})

Routes and Grouping

Routes can have multiple handlers, called middleware, which might be useful in grouping the requests and doing preliminary work before some routes. For example, the following grouping and routing is valid:

type simpleHandler struct{}

func (s *simpleHandler) Handle(c *Context) error {
	c.SetBody("hi")
	return nil
}

// The middle ware that will fail if you supply 5 as a user ID
type middlewareFailIfUserId5 struct {
	Param struct {
		UserID int
	}
}

func (m *middlewareFailIfUserId5) Handle(c *Context) error {
	if m.Param.UserID == 5 {
		c.Status(http.StatusTeapot)
		c.SetBody("Sorry")
		c.StopChain()
	}
	return nil
}

r := e.GetRouter()

g := r.Group("/api/user/:UserID", &middlewareFailIfUserId5{})
g.GET("/name", &simpleHandler{})
g.GET("/wow", &simpleHandler{})

/* 
 The example responses:

 /api/user/5/name -> Sorry 
 /api/user/4/name -> hi
 /api/user/1/wow  -> hi
*/

Field Validation

We use asaskevich/govalidator as a validation framework. If the supplied input does not pass the validation step, http.StatusBadRequest (400) is returned the user with the cause. Validation can be used in Query, Param, Body or Form type inputs. An example can be seen as follows:

type QueryParamHandler struct {
    Query struct {
        Username string `valid:"alpha"`
        Age int
    }
}
func(p *QueryParamHandler) Handle(c *Context) error {
    println(p.Param.Age)
    c.SetBody(p.Param.Username)
    return nil
}

If a request with a non valid username field is set, it returns a ParseError.

Dependency Injection

One of the thing that makes gongular from other frameworks is that it provides safe value injection to route handlers. It can be used to store database connections, or some other external utility that you want that to be avilable in your handler, but do not want to make it global, or just get it from some other global function that might pollute the space. Supplied dependencies are provided as-is to route handlers and they are private to supplied router, nothing is global.

Basic Injection

Gongular allows very basic injection: You provide a value to gongular.Engine, and it provides you to your handler if you want it in your handler function. It is not like a Guice or Spring like injection, it does not resolve dependencies of the injections, it just provides the value, so that you do not use global values, and it makes the testing easier, since you can just test your handler function by mocking the interfaces you like.

type myHandler struct {
	Param struct {
		UserID uint
	}
	Database *sql.DB
}

func (i *myHandler) Handle(c *Context) error {
	c.SetBody(fmt.Sprintf("%p:%d", i.Database, i.Param.UserID))
	return nil
}

db := new(sql.DB)
e.Provide(db)
e.GetRouter().GET("/my/db/interaction/:UserID", &myHandler{})

Keyed Injection

The basic injection works great, but if you want to supply same type of value more than once, you have to use keyed injection so that gongular can differ.

type injectKey struct {
	Val1 int `inject:"val1"`
	Val2 int `inject:"val2"`
}

func (i *injectKey) Handle(c *Context) error {
	c.SetBody(i.Val1 * i.Val2)
	return nil
}

e.ProvideWithKey("val1", 71)
e.ProvideWithKey("val2", 97)

e.GetRouter().GET("/", &injectKey{})

Custom Injection

Sometimes, providing values as is might not be sufficient for you. You can chose to ping the database, create a transaction, get a value from a pool, and these requires implementing a custom logic. Gongular allows you to write a CustomProvideFunction which allows you to provide your preferred value with any logic you like.

type injectCustom struct {
	DB *sql.DB
}

func (i *injectCustom) Handle(c *Context) error {
	c.SetBody(fmt.Sprintf("%p", i.DB))
	return nil
}

e := newEngineTest()

var d *sql.DB
e.CustomProvide(&sql.DB{}, func(c *Context) (interface{}, error) {
    d = new(sql.DB)
    return d, nil
})

e.GetRouter().GET("/", &injectCustom{})

Unsafe Injection

The default Provide functions allow you to inject implementations only. Injection of interfaces will not work. During injection, the injector will search for a provided type and fail. For example the following code will not work:

type injectKey struct {
	DB MySQLInterface `inject:"db"`
}

func (i *injectKey) Handle(c *Context) error {
	c.SetBody("yay")
	return nil
}

e.ProvideWithKey("db", &sql.DB{})

e.GetRouter().GET("/", &injectKey{})

This will cause an injector error. If you want to inject interfaces you must use ProvideUnsafe. ProvideUnsafe is a strict key/value injection. You cannot provide multiple values for the same key.

Example usage:

type injectKey struct {
	DB MySQLInterface `inject:"db"`
}

func (i *injectKey) Handle(c *Context) error {
	c.SetBody("yay")
	return nil
}

e.ProvideUnsafe("db", initializeDB())

// This would cause a panic
// e.ProvideUnsafe("db", &sql.DB{})

e.GetRouter().GET("/", &injectKey{})

gongular.Context struct

  • context.SetBody(interface{}) : Sets the response body to be serialized.
  • context.Status(int) : Sets the status of the response if not previously set
  • context.MustStatus(int) : Overrides the previously written status
  • context.Request() : Returns the underlying raw HTTP Request
  • context.Header(string,string) : Sets a given response header.
  • context.Finalize() : Used to write the response to client, normally should not be used other than in PanicHandler since gongular takes care of the response.
  • context.Logger() : Returns the logger of the context.

Route Callback

The route callback, set globally for the engine, allows you to get the stats for the completed request. It contains common info, including the request logs and the matched handlers, how much time it took in each handler, the total time, the total response size written and the final status code, which can be useful for you to send it to another monitoring service, or just some Elasticsearch for log analysis.

type RouteStat struct {
    Request       *http.Request
    Handlers      []HandlerStat
    MatchedPath   string
    TotalDuration time.Duration
    ResponseSize  int
    ResponseCode  int
    Logs          *bytes.Buffer
}

Error Handler

In case you return an error from your function, or another error occurs which makes the request unsatisfiable, gongular.Engine calls the error handler function, in which defaults to the following handler:

var defaultErrorHandler = func(err error, c *Context) {
	c.logger.Println("An error has occurred:", err)

	switch err := err.(type) {
	case InjectionError:
		c.MustStatus(http.StatusInternalServerError)
		c.logger.Println("Could not inject the requested field", err)
	case ValidationError:
		c.MustStatus(http.StatusBadRequest)
		c.SetBody(map[string]interface{}{"ValidationError": err})
	case ParseError:
		c.MustStatus(http.StatusBadRequest)
		c.SetBody(map[string]interface{}{"ParseError": err})
	default:
		c.SetBody(err.Error())
		c.MustStatus(http.StatusInternalServerError)
	}

	c.StopChain()
}

WebSockets

Gongular supports websocket connections as well. The handler function is similar to regular route handler interface, but it also allows connection termination if you wish with the Before handler.

type WebsocketHandler interface {
	Before(c *Context) (http.Header, error)
	Handle(conn *websocket.Conn)
}

First of all, handle function does not return an error, since it is a continuous execution. User is responsible for all the websocket interaction. Secondly, Before is a filter applied just before upgrading the request to websocket. It can be useful for filtering the request and returning an error would not open a websocket but close it with an error. The http.Header is for answering with a http.Header which allows setting a cookie. Can be omitted if not desired.

The nice thing about WebsocketHandler is that it supports Param and Query requests as well, so that all the binding and validation can be done before the request, and you can use it in your handler.

type wsTest struct {
	Param struct {
		UserID int
	}
	Query struct {
		Track    bool
		Username string
	}
}

func (w *wsTest) Before(c *Context) (http.Header, error) {
	return nil, nil
}

func (w *wsTest) Handle(conn *websocket.Conn) {
	_, msg, err := conn.ReadMessage()
	if err != nil {
		conn.Close()
	}

	toSend := fmt.Sprintf("%s:%d:%s:%t", msg, w.Param.UserID, w.Query.Username, w.Query.Track)
	conn.WriteMessage(websocket.TextMessage, []byte(toSend))
	conn.Close()
}
Comments
  • dependency injection does not appear to work with interfaces

    dependency injection does not appear to work with interfaces

    func main() {
        engine := gongular.NewEngine()
    
        var db store.Store = store.NewStore()
        engine.Provide(db)
    
        engine.GetRouter().GET("/", &MyHandler{})
        engine.ListenAndServe(":3000")
        // ... rest of code
    }
    
    type MyHandler struct {
        DB store.Store // <- an interface
    }
    
    func (c *MyHandler) Handle(ctx *gongular.Context) error {
    	ctx.SetBody("yay")
    	return nil
    }
    

    With this code I am getting

    Could not inject type db with key store.Store because No such dependency exists

    If I change the property from an interface to the implementation of store.Store it works. I also tried with ProvideWithKey and it yielded the same results.

    The problem is here. reflect.TypeOf will always return the underlying type even if I provide an interface.

    opened by blockloop 4
  • Support more http methods

    Support more http methods

    Currently only GET, POST, PUT, and HEAD are supported. DELETE is also a commonly used method. Given the limited set of actual methods you can easily implement all of them.

    opened by blockloop 0
  • Any possiblity of OpenAPI / Swagger support?

    Any possiblity of OpenAPI / Swagger support?

    The OpenAPI Specification, formerly known as the Swagger Specification, is a major standard for defining RESTful interfaces and allows consumers to autogenerate an SDK from our REST api in whatever language they want. https://swagger.io

    Support for auto generated documentation would be a great feature of gongular given how easy it is to setup REST services and define all the type information for validation (which could be read by swagger).

    The https://github.com/swaggo/swag project provides parsers for a number of libraries like gin, echo, & buffalo. Any chance support for gongular could be provided?

    opened by housingdreams 1
  • Fix function comments based on best practices from Effective Go

    Fix function comments based on best practices from Effective Go

    Every exported function in a program should have a doc comment. The first sentence should be a summary that starts with the name being declared. From effective go.

    I generated this with CodeLingo and I'm keen to get some feedback, but this is automated so feel free to close it and just say "opt out" to opt out of future CodeLingo outreach PRs.

    opened by BlakeMScurr 1
  • How to handle non-JSON responses based on Handler?

    How to handle non-JSON responses based on Handler?

    For some of my http.Handlers I would like to have validation (or other errors) sent back inside a html/template (instead of simply output as JSON).

    The Context struct is passed to the error handler.

    var defaultErrorHandler = func(err error, c *gongular.Context) {`
    

    My first thought was to figure out how to use the Context as a middleman so that I could look for a flag set by the controller/handler. Then I could write an ErrorHandler that would return JSON or another format depending on if a flag was set. However, c *gongular.Context does not seem to provide such a flagging mechanism.

    What is the correct way to support outputs other than JSON? Is there a way for a middleware to handle this?

    Update: it looks like a middleware can call .StopChain() to avoid the ErrorHandler. However, you can't seem to access other handlers/middlewares from a middleware.

    type multiParam struct {
    	Param struct {
    		UserID string
    		Page   int
    	}
    }
    
    func (m *multiParam) Handle(c *gongular.Context) error {
    	fmt.Println("multiparam")
    	c.SetBody(fmt.Sprintf("%s:%d", m.Param.UserID, m.Param.Page))
    	return nil
    }
    
    // The middleware to intercept validation errors
    type middlewareHandler struct {}
    
    func (m *middlewareHandler) Handle(c *gongular.Context) error {
    	fmt.Println("middleware")
    	return nil
    }
    
    func main() {
    	e := newEngineTest()
    	g := e.GetRouter().Group("/user/:UserID/page", &middlewareHandler{})
    	g.GET("/:Page", &multiParam{})
    }
    
    opened by Xeoncross 2
  • Lowercase Struct Param Fields?

    Lowercase Struct Param Fields?

    Currently Body/Form/Query fields must be capital to be public struct members.

    "reflect: reflect.Value.SetString using value obtained using unexported field" - gongular/parser.go:142

    type InputHandler struct {
    	Param struct {
    		Name string
    	}
    	Query struct {
    		Ref int
    	}
    	Form struct {
    		Title   string `valid:"required"`
    		Email   string
    		Message string `valid:"required"`
    	}
    }
    

    However, it would be nice if lowercase values could be used to conform to modern lowercase request param usage. I've looked through the source code and documentation and I don't see any mention of (my first guess) struct field tags Title string 'name:title' as an option.

    How should I be handling this?

    opened by Xeoncross 2
  • Support html/template

    Support html/template

    It looks like there is no option for returning HTML/templates.

    Can we add a check for this?

    type IndexHandler struct{}
    
    func (m *IndexHandler) Handle(c *gongular.Context) (err error) {
    	var indexHTML = `<h1>Hello World</h1>`
    	var tmpl *template.Template
    	tmpl, err = template.New("index").Parse(indexHTML)
    	c.SetBody(tmpl)
    	return
    }
    

    The work around is to manually set the header and return []byte.

    c.Header("Content-Type", "text/html")
    c.SetBody([]byte(indexHTML))
    

    Possible Solution:

    if v, ok := a.(*template.Template); ok {
    	c.w.WriteHeader(c.status)
    	err := v.Execute(c.w)
    	if err != nil {
    		c.logger.Println("Could not write the response", err)
    	}
    	return 1 // todo?
    }
    
    opened by Xeoncross 2
Releases(v2.1)
Owner
Mustafa Akın
Mustafa Akın
Simple REST API to get time from many different timezones

Timezone API Simple REST API for getting current time in different timezones. This is the first assignment from Rest-Based Microservices API Developme

Irfan Sofyana Putra 0 Oct 31, 2021
Flamingo Framework and Core Library. Flamingo is a go based framework for pluggable web projects. It is used to build scalable and maintainable (web)applications.

Flamingo Framework Flamingo is a web framework based on Go. It is designed to build pluggable and maintainable web projects. It is production ready, f

Flamingo 343 Jan 5, 2023
⚡ Rux is an simple and fast web framework. support middleware, compatible http.Handler interface. 简单且快速的 Go web 框架,支持中间件,兼容 http.Handler 接口

Rux Simple and fast web framework for build golang HTTP applications. NOTICE: v1.3.x is not fully compatible with v1.2.x version Fast route match, sup

Gookit 84 Dec 8, 2022
Golanger Web Framework is a lightweight framework for writing web applications in Go.

/* Copyright 2013 Golanger.com. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except

golanger 298 Nov 14, 2022
re:Web enables classic web applications to run on AWS Lambda.

re:Web re:Web enables classic web applications to run on AWS Lambda. re:Web interfaces with the Lambda Runtime API. It translates API Gateway requests

null 106 Jan 1, 2023
Roche is a Code Generator and Web Framework, makes web development super concise with Go, CleanArch

It is still under development, so please do not use it. We plan to release v.1.0.0 in the summer. roche is a web framework optimized for microservice

Riita 14 Sep 19, 2022
The jin is a simplified version of the gin web framework that can help you quickly understand the core principles of a web framework.

jin About The jin is a simplified version of the gin web framework that can help you quickly understand the core principles of a web framework. If thi

null 8 Jul 14, 2022
A powerful go web framework for highly scalable and resource efficient web application

webfr A powerful go web framework for highly scalable and resource efficient web application Installation: go get -u github.com/krishpranav/webfr Exa

Krisna Pranav 13 Nov 28, 2021
A powerful go web framework for highly scalable and resource efficient web application

A powerful go web framework for highly scalable and resource efficient web application

null 22 Oct 3, 2022
Chainrand contract + web frontend + web backend

Chainrand-web This repo contains the implementation of Chainrand. https://chainrand.io Smart Contract Contains functionality to tie a Chainlink VRF to

Chainrand 1 Dec 8, 2021
A web app built using Go Buffalo web framework

Welcome to Buffalo Thank you for choosing Buffalo for your web development needs. Database Setup It looks like you chose to set up your application us

Mike Okoth 0 Feb 7, 2022
Gin is a HTTP web framework written in Go (Golang).

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.

Gin-Gonic 65.5k Jan 3, 2023
An ideally refined web framework for Go.

Air An ideally refined web framework for Go. High-performance? Fastest? Almost all web frameworks are using these words to tell people that they are t

Aofei Sheng 421 Dec 15, 2022
An opinionated productive web framework that helps scaling business easier.

appy An opinionated productive web framework that helps scaling business easier, i.e. focus on monolith first, only move to microservices with GRPC la

appist 128 Nov 4, 2022
BANjO is a simple web framework written in Go (golang)

BANjO banjo it's a simple web framework for building simple web applications Install $ go get github.com/nsheremet/banjo Example Usage Simple Web App

Nazarii Sheremet 20 Sep 27, 2022
beego is an open-source, high-performance web framework for the Go programming language.

Beego Beego is used for rapid development of enterprise application in Go, including RESTful APIs, web apps and backend services. It is inspired by To

astaxie 592 Jan 1, 2023
High performance, minimalist Go web framework

Supported Go versions As of version 4.0.0, Echo is available as a Go module. Therefore a Go version capable of understanding /vN suffixed imports is r

LabStack LLC 24.6k Jan 2, 2023
⚡️ Express inspired web framework written in Go

Fiber is an Express inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. Designed to ease things up for fast development w

Fiber 23.8k Jan 2, 2023