Skip to content

hariadivicky/nano

Repository files navigation

Nano HTTP Multiplexer

Go Report Card GitHub issues GitHub code size in bytes GitHub

Nano is a simple & elegant HTTP request 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

Nano is 100% compatible with standard go net/http package, you can also attach nano as http.Server handler.

    app := nano.New()
    // ...
    server := &http.Server{
        WriteTimeout: 10 * time.Second,
        ReadTimeout:  10 * time.Second,
        IdleTimeout:  30 * time.Second,
        Handler:      app, // attach Nano as server handler.
        Addr:         ":8000",
    }

    server.ListenAndServe()

API Usages

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

Using HEAD, OPTIONS, GET, POST, PUT, PATCH, and DELETE

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

    app.HEAD("/someHead", headHandler)
    app.OPTIONS("/someOptions", optionsHandler)
    app.GET("/someGet", getHandler)
    app.POST("/somePost", postHandler)
    app.PUT("/somePut", putHandler)
    app.PATCH("/somePatch", patchHandler)
    app.DELETE("/someDelete", deleteHandler)

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

Default Route Handler

You can register your own default handler. The default handler called when there is no matching route. If you doesn't set the default handler, nano will register 404 response text 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."
    })
})

Route 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)
    })
}

Static File Server

You can use *nano.Static() function to serve static files like html, css, and js in your server.

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

    assetDir := http.Dir("./statics")
    app.Static("/assets", assetDir)

    // now your static files are accessible via http://yourhost.com/assets/*
}

Request Binding

To use request binding you must provide form tag to each field in your struct. You can also add the validation rules using validate tag. to see more about available validate tag value, visit Go Validator

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" validate:"required"`
}

Bind function will returns *nano.BindingError when an error occured due to deserialization error or validation error. The description about error fields will be stored in err.Fields.

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)
})

Bind function 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 can use this functions:

Bind URL Query

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

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

Bind Multipart Form

You can use BindMultipartForm to bind request body with multipart/form-data type

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

Bind JSON

if you have request with application/json type, you can bind it using BindJSON function

var cart ShoppingCart
err := c.BindJSON(&cart)

BindJSON can also parsing your RFC3339 date/time format to another format by adding time_format in your field tag. You can read more at jsontime docs.

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 which 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 can make routes grouping which it 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 implements nano.HandlerFunc, you can forward the request to next handler 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 in certain route

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

You can chains the middlewares (handler)

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

Middleware Group

Using middleware in 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")
}

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 (using 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, gzip compressor, and recovery middleware.

Recovery Middleware

Recovery middleware recovers server from panic.

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

CORS middleware handles 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 compresses http response using gzip encoding.

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 projects list using Nano web framework.

License

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