Skip to content

hariadivicky/nano

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nano HTTP Multiplexer

Go Report Card GitHub issues GitHub code size in bytes GitHub

Nano is a simple & elegant HTTP multiplexer written in Go (Golang). It features REST API with Go net/http performance. If you need a minimalist, productivity, and love simplicity, Nano is great choice.

Contents

Installation

To install Nano package, you need to install Go and set your Go workspace first.

  • The first need Go installed (version 1.11+ is required), then you can use the below Go command to install Nano.
go get -u github.com/hariadivicky/nano
  • Import it in your code:
import "github.com/hariadivicky/nano"
  • Import net/http. This is optional for http status code like http.StatusOK.
import "net/http"

Quick start

# assume the following codes in example.go file
$ cat example.go
package main

import (
 "fmt"
 "net/http"

 "github.com/hariadivicky/nano"
)

func main() {
    app := nano.New()

    // simple endpoint to print hello world.
    app.GET("/", func(c *nano.Context) {
        c.String(http.StatusOK, "hello world\n")
    })

    app.Run(":8080")
}
# run example.go and visit http://localhost:8080 on your browser
$ go run example.go

API Usages

You can find a ready-to-run examples at Todo List Example.

Using GET, POST, PUT, and DELETE

func main() {
    // Creates a nano router
    app := nano.New()

    app.GET("/someGet", getHandler)
    app.POST("/somePost", postHandler)
    app.PUT("/somePut", putHandler)
    app.DELETE("/someDelete", deleteHandler)

    // Run apps.
    app.Run(":8080")
}

Default Route Handler

You could register you own default handler, the default handler will called when there is not matching route found. If you doesn't set the default handler, nano will register default 404 response as default handler.

app.Default(func(c *nano.Context) {
    c.JSON(http.StatusNotFound, nano.H{
        "status": "error",
        "message": "Oh no, this API endpoint does not exists."
    })
})

Router parameter

Get route parameter using c.Param(key)

func main() {
    app := nano.New()

    // This handler will match /products/1 but will not match /products/ or /products
    app.GET("/products/:productId", func(c *nano.Context) {
        productId := c.Param("productId") //string
        c.String(http.StatusOK, "you requested %s", productId)
    })
}

Request Binding

To use request binding you must provide "form" or "json" tag to each field in your struct. You can also add rules:"required" to marks a field as required for binding validation

type Address struct {
    Street     string `form:"street" json:"street"`
    PostalCode string `form:"postal_code" json:"postal_code"`
    CityID     int    `form:"city_id" json:"city_id" rules:"required"`
}

By calling c.Bind function, it's will returns *nano.BindingError when an error occured due to deserialization error or validation error

app.GET("/address", func(c *nano.Context) {
    var address Address
    if err := c.Bind(&address); err != nil {
        c.String(err.HTTPStatusCode, err.Message)
        return
    }

    c.String(http.StatusOK, "city id: %d, postal code: %s", address.CityID, address.PostalCode)
})

c.Bind automatically choose deserialization source based on your request Content-Type and request method. GET and HEAD methods will try to bind url query or urlencoded form. Otherwise, it will try to bind multipart form or json.

but if you want to manually choose the binding source, you could uses this functions below:

Bind URL Query

If you want to bind url query like page=1&limit=50 or urlencoded form you could use nano.BindSimpleForm

var paging Pagination
err := nano.BindSimpleForm(c.Request, &paging)

Bind Multipart Form

You could use nano.BindMultipartForm to bind request body with multipart/form-data type

var post BlogPost
err := nano.BindMultipartForm(c.Request, &post)

Bind JSON

if you have request with application/json type, you could bind it using nano.BindJSON function

var schema ComplexSchema
err := nano.BindJSON(c.Request, &schema)

Error Binding

Each you call Bind, BindSimpleForm, BindMultipartForm, and BindJSON it's always returns *nano.ErrorBinding, except when binding success without any errors it returns nil. ErrorBinding has two field that are HTTPStatusCode & Message. Here is the details:

HTTPStatusCode Reason
1 500 Conversion Error or Give non-pointer to target struct parameter
2 420 Validation Error
3 400 Deserialization Error

ErrorBinding.HTTPStatusCode is useful to determine response code

Grouping Routes

You could use grouping routes that have same prefix or using same middlewares

app := nano.New()

// simple endpoint to print hello world.
app.GET("/", func(c *nano.Context) {
    c.String(http.StatusOK, "hello world\n")
})

// path: /api/v1
apiv1 := app.Group("/api/v1")
{
    // path: /api/v1/finances
    finance := apiv1.Group("/finances")
    {
        // path: /api/v1/finances/report/1
        finance.GET("/report/:period", reportByPeriodHandler)
        // path: /api/v1/finances/expenses/voucher
        finance.POST("/expenses/voucher", createExpenseVoucherHandler)
    }

    // path: /api/v1/users
    users := apiv1.Group("/users")
    {
        // path: /api/v1/users
        users.POST("/", registerNewUserHandler)
        // path: /api/v1/users/1/detail
        users.GET("/:id/detail", showUserDetailHandler)
    }
}

Writing Middleware

Middleware implement nano.HandlerFunc, you could forward request by calling c.Next()

// LoggerMiddleware functions to log every request.
func LoggerMiddleware() nano.HandlerFunc {
    return func(c *nano.Context) {
        // before middleware.
        start := time.Now()
        log.Println(c.Method, c.Path)

        // forward to next handler.
        c.Next()

        // after middleware.
        log.Printf("request complete in %s", time.Since(start))
    }
}

Using Middleware

Using middleware on certain route

app.GET("/change-password", verifyTokenMiddleware(), changePasswordHandler)

You could chaining middleware or handler

app.GET("/secret", verifyStepOne(), verifyStepTwo(), grantAccessHandler, logChangeHandler)

Middleware Group

Using middleware on router group

app := nano.New()
// apply to all routes.
app.Use(globalMiddleware())

v1 := app.Group("/v1")
v1.Use(onlyForV1())
{
 // will apply to v1 routes.
}

Nano Context

Nano Context is wrapper for http request and response. this example will use c variable as type of *nano.Context

Request

Get request method & path

log.Println(c.Method, c.Path)

Get field value from request body

username := c.PostForm("username")

Get field value with default value from request body

status := c.PostFormDefault("status", "active")

Get url query

page := c.Query("page")

Get url query with default value

page := c.QueryDefault("page", "1")

You could check if client need JSON response

func main() {
    app := nano.New()

    // simple endpoint to print hello world.
    app.GET("/", func(c *nano.Context) {

        // client request json response.
        if c.ExpectJSON() {
            c.JSON(http.StatusOK, nano.H{
                "message": "hello world",
            })

            return
        }

        c.String(http.StatusOK, "hello world\n")
    })

    app.Run(":8080")
}

Parsing json body request

type person struct {
    Name string
    Age  int
}

func main() {
    app := nano.New()

    // simple endpoint to print hello world.
    app.POST("/person", func(c *nano.Context) {
        if !c.IsJSON() {
            c.String(http.StatusBadRequest, "server only accept json request.")
        }

        form := new(person)
        // parse json body.
        err := c.ParseJSONBody(form)

        if err != nil {
            c.String(http.StatusBadRequest, "bad request.")
        }

        c.String(http.StatusOK, "hello %s your age is %d \n", form.Name, form.Age)
    })

    app.Run(":8080")
}

Response

Set response header & content type

c.SetHeader("X-Powered-By", "Nano HTTP Multiplexer")
c.SetContetType("image/png")

Plain text response

c.String(http.StatusNotFound, "404 not found: %s", c.Path)

JSON response (with nano object wrapper)

c.JSON(http.StatusOK, nano.H{
    "status":  "pending",
    "message": "data stored",
})

HMTL response

c.HTML(http.StatusOK, "<h1>Hello There!</h1>")

Binary response

c.Data(http.StatusOK, binaryData)

Nano Middlewares

Nano has shipped with some default middleware like cors and recovery middleware.

Recovery Middleware

Recovery middleware is functions to recover server when panic was fired.

func main() {
    app := nano.New()
    app.Use(nano.Recovery())

    app.GET("/", func(c *nano.Context) {
        stack := make([]string, 0)

        c.String(http.StatusOK, "100th stack is %s", stack[99])
    })

    app.Run(":8080")
}

CORS Middleware

This middleware is used to deal with cross-origin request.

func main() {
    app := nano.New()

    // Only allow from :3000 and google.
    cors := nano.CORSWithConfig(nano.CORSConfig{
        AllowedOrigins: []string{"http://localhost:3000", "https://wwww.google.com"},
        AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPut},
        AllowedHeaders: []string{nano.HeaderContentType, nano.HeaderAccept},
    })

    app.Use(cors)

    // ...
}

Gzip Middleware

Gzip middleware is used for http response compression.

func main() {
    app := nano.New()

    app.Use(nano.Gzip(gzip.DefaultCompression))

    // ...
}

don't forget to import compress/gzip package for compression level at this example. available compression levels are: gzip.NoCompression, gzip.BestSpeed, gzip.BestCompression, gzip.DefaultCompression, and gzip.HuffmanOnly

Users

Awesome project lists using Nano web framework.

License

Nano using MIT license. Please read LICENSE files for more information about nano license.