README

GoDoc Version Build Status Coverage Status Go Report Card

YARF: Yet Another REST Framework

YARF is a fast micro-framework designed to build REST APIs and web services in a fast and simple way. Designed after Go's composition features, takes a new approach to write simple and DRY code.

Getting started

Here's a transcription from our examples/simple package. This is a very simple Hello World web application example.

package main

import (
    "github.com/yarf-framework/yarf"
)

// Define a simple resource
type Hello struct {
    yarf.Resource
}

// Implement the GET method
func (h *Hello) Get(c *yarf.Context) error {
    c.Render("Hello world!")
    
    return nil
}

// Run app server on http://localhost:8080
func main() {
    y := yarf.New()
    
    y.Add("/", new(Hello))
    
    y.Start(":8080")
}

For more code and examples demonstrating all YARF features, please refer to the 'examples' directory.

Features

Struct composition based design

YARF resources are custom structs that act as handlers for HTTP methods. Each resource can implement one, several or all HTTP methods needed (GET, POST, DELETE, etc.). Resources are created using Go's struct composition and you only have to declare the yarf.Resource type into your own struct.

Example:


import "github.com/yarf-framework/yarf"

// Define a simple resource
type Hello struct {
    yarf.Resource
}

// Implement the GET method
func (h *Hello) Get(c *yarf.Context) error {
    c.Render("Hello world!")
    
    return nil
}

Simple router

Using a strict match model, it matches exact URLs against resources for increased performance and clarity during routing. The routes supports parameters in the form '/:param'.

The route:

/hello/:name

Will match:

/hello/Joe
/hello/nobody
/hello/somebody/
/hello/:name
/hello/etc

But it won't match:

/
/hello
/hello/Joe/AndMark
/Joe/:name
/any/thing

You can define optional parameters using multiple routes on the same Resource.

Route parameters

At this point you know how to define parameters in your routes using the /:param naming convention. Now you'll see how easy is to get these parameters by their name from your resources using the Context.Param() method.

Example:

For the route:

/hello/:name

You can have this resource:

import "github.com/yarf-framework/yarf"

type Hello struct {
    yarf.Resource
}

func (h *Hello) Get(c *yarf.Context) error {
    name := c.Param("name")

    c.Render("Hello, " + name)

    return nil
}

Route wildcards

When some extra freedom is needed on your routes, you can use a * as part of your routes to match anything where the wildcard is present.

The route:

/something/*/here

Will match the routes

/something/is/here
/something/happen/here
/something/isnt/here
/something/was/here

And so on...

You can also combine this with parameters inside the routes for extra complexity.

Catch-All wildcard

When using the * at the end of any route, the router will match everything from the wildcard and forward.

The route:

/match/from/here/*

Will match:

/match/from/here
/match/from/here/please
/match/from/here/and/forward
/match/from/here/and/forward/for/ever/and/ever

And so on...

Note about the wildcard

The * can only be used by itself and it doesn't works for single character matching like in regex.

So the route:

/match/some*

Will NOT match:

/match/some
/match/something
/match/someone
/match/some/please
Context

The Context object is passed as a parameter to all Resource methods and contains all the information related to the ongoing request.

Check the Context docs for a reference of the object: https://godoc.org/github.com/yarf-framework/yarf#Context

Middleware support

Middleware support is implemented in a similar way as Resources, by using composition.
Routes will be pre-filtered and post-filtered by Middleware handlers when they're inserted in the router.

Example:

import "github.com/yarf-framework/yarf"

// Define your middleware and composite yarf.Middleware
type HelloMiddleware struct {
    yarf.Middleware
}

// Implement only the PreDispatch method, PostDispatch not needed.
func (m *HelloMiddleware) PreDispatch(c *yarf.Context) error {
    c.Render("Hello from middleware! \n") // Render to response.

    return nil
}

// Insert your middlewares to the server
func main() {
    y := yarf.New()

    // Add middleware
    y.Insert(new(HelloMiddleware))
    
    // Define routes
    // ...
    // ...
    
    // Start the server
    y.Start()
}
Route groups

Routes can be grouped into a route prefix and handle their own middleware.

Nested groups

As routes can be grouped into a route prefix, other groups can be also grouped allowing for nested prefixes and middleware layers.

Example:

import "github.com/yarf-framework/yarf"

// Entry point of the executable application
// It runs a default server listening on http://localhost:8080
//
// URLs after configuration:
// http://localhost:8080
// http://localhost:8080/hello/:name
// http://localhost:8080/v2
// http://localhost:8080/v2/hello/:name
// http://localhost:8080/extra/v2
// http://localhost:8080/extra/v2/hello/:name
//
func main() {
    // Create a new empty YARF server
    y := yarf.New()

    // Create resources
    hello := new(Hello)
    hellov2 := new(HelloV2)

    // Add main resource to multiple routes at root level.
    y.Add("/", hello)
    y.Add("/hello/:name", hello)

    // Create /v2 prefix route group
    g := yarf.RouteGroup("/v2")

    // Add /v2/ routes to the group
    g.Add("/", hellov2)
    g.Add("/hello/:name", hellov2)

    // Use middleware only on the /v2/ group
    g.Insert(new(HelloMiddleware))

    // Add group to Yarf routes
    y.AddGroup(g)

    // Create another group for nesting into it.
    n := yarf.RouteGroup("/extra")

    // Nest /v2 group into /extra/v2
    n.AddGroup(g)

    // Use another middleware only for this /extra/v2 group
    n.Insert(new(ExtraMiddleware))

    // Add group to Yarf
    y.AddGroup(n)

    // Start server listening on port 8080
    y.Start(":8080")
}

Check the ./examples/routegroups demo for the complete working implementation.

Route caching

A route cache is enabled by default to improve dispatch speed, but sacrificing memory space. If you're running out of RAM memory and/or your app has too many possible routes that may not fit, you should disable the route cache.

To enable/disable the route cache, just set the UseCache flag of the Yarf object:

y := yarf.New()
y.UseCache = false
Chain and extend

Just use the Yarf object as any http.Handler on a chain. Set another http.Handler on the Yarf.Follow property to be followed in case this Yarf router can't match the request.

Here's an example on how to follow the request to a public file server:

package main 

import (
    "github.com/yarf-framework/yarf"
    "net/http"
)

func main() {
    y := yarf.New()

    // Add some routes
    y.Add("/hello/:name", new(Hello))
    
    //... more routes here
    
    // Follow to file server
    y.Follow = http.FileServer(http.Dir("/var/www/public"))
    
    // Start the server
    y.Start(":8080")
}
Custom NotFound error handler

You can handle all 404 errors returned by any resource/middleware during the request flow of a Yarf server. To do so, you only have to implement a function with the func(c *yarf.Context) signature and set it to your server's Yarf.NotFound property.

y := yarf.New()

// ...

y.NotFound = func(c *yarf.Context) {
    c.Render("This is a custom Not Found handler")
}

// ...

Performance

On initial benchmarks, the framework seems to perform very well compared with other similar frameworks. Even when there are faster frameworks, under high load conditions and thanks to the route caching method, YARF seems to perform as good or even better than the fastests that work better under simpler conditions.

Check the benchmarks repository to run your own:

https://github.com/yarf-framework/benchmarks

HTTPS support

Support for running HTTPS server from the net/http package.

Using the default server
func main() {
    y := yarf.New()
    
    // Setup the app
    // ...
    // ...
    
    // Start https listening on port 443
    y.StartTLS(":443", certFile, keyFile)
}

Using a custom server
func main() {
    y := yarf.New()

    // Setup the app
    // ...
    // ...

    // Configure custom http server and set the yarf object as Handler.
    s := &http.Server{
        Addr:           ":443",
        Handler:        y,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServeTLS(certFile, keyFile)
}

Why another micro-framework?

Why not?

No, seriously, i've researched for small/fast frameworks in the past for a Go project that I was starting. I found several options, but at the same time, none seemed to suit me. Some of them make you write weird function wrappers to fit the net/http package style. Actually, most of them seem to be function-based handlers. While that's not wrong, I feel more comfortable with the resource-based design, and this I also feel aligns better with the spirit of REST.

In Yarf you create a resource struct that represents a REST resource and it has all HTTP methods available. No need to create different routes for GET/POST/DELETE/etc. methods.

By using composition, you don't need to wrap functions inside functions over and over again to implement simple things like middleware or extension to your methods. You can abuse composition to create a huge OO-like design for your business model without sacrifying performance and code readability.

Even while the code style differs from the net/http package, the framework is fully compatible with it and couldn't run without it. Extensions and utilities from other frameworks or even the net/http package can be easily implemented into Yarf by just wraping them up into a Resource, just as you would do on any other framework by wrapping functions.

Context handling also shows some weird designs across frameworks. Some of them rely on reflection to receive any kind of handlers and context types. Others make you receive some extra parameter in the handler function that actually brokes the net/http compatibility, and you have to carry that context parameter through all middleware/handler-wrapper functions just to make it available. In Yarf, the Context is automatically sent as a parameter to all Resource methods by the framework.

For all the reasons above, among some others, there is a new framework in town.

Documentation

Index

Constants

View Source
const Version = "0.8.5"

Version string

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache struct {

	// Sync Mutex
	sync.RWMutex
	// contains filtered or unexported fields
}

Cache is the service handler for route caching

func NewCache

func NewCache() *Cache

NewCache creates and initializes a new Cache service object.

func (*Cache) Get

func (c *Cache) Get(k string) (rc RouteCache, ok bool)

Get retrieves a routeCache object by key name.

func (*Cache) Set

func (c *Cache) Set(k string, r RouteCache)

Set stores a routeCache object under a key name.

type Context

type Context struct {
	// The *http.Request object as received by the HandleFunc.
	Request *http.Request

	// The http.ResponseWriter object as received by the HandleFunc.
	Response http.ResponseWriter

	// Parameters received through URL route
	Params Params

	// Free storage to be used freely by apps to their convenience.
	Data ContextData
	// contains filtered or unexported fields
}

Context is the data/status storage of every YARF request. Every request will instantiate a new Context object and fill in with all the request data. Each request Context will be shared along the entire request life to ensure accesibility of its data at all levels.

func NewContext

func NewContext(r *http.Request, rw http.ResponseWriter) *Context

NewContext creates a new *Context object with default values and returns it.

func (*Context) FormValue

func (c *Context) FormValue(name string) string

FormValue is a wrapper for c.Request.Form.Get() and it calls c.Request.ParseForm().

func (*Context) GetClientIP

func (c *Context) GetClientIP() (ip string)

GetClientIP retrieves the client IP address from the request information. It detects common proxy headers to return the actual client's IP and not the proxy's.

func (*Context) Param

func (c *Context) Param(name string) string

Param is a wrapper for c.Params.Get()

func (*Context) QueryValue

func (c *Context) QueryValue(name string) string

QueryValue is a wrapper for c.Request.URL.Query().Get().

func (*Context) Redirect

func (c *Context) Redirect(url string, code int)

Redirect sends the corresponding HTTP redirect response with the provided URL and status code. It's just a wrapper for net/http.Redirect()

func (*Context) Render

func (c *Context) Render(content string)

Render writes a string to the http.ResponseWriter. This is the default renderer that just sends the string to the client. Check other Render[Type] functions for different types.

func (*Context) RenderGzip

func (c *Context) RenderGzip(content []byte) error

RenderGzip takes a []byte content and if the client accepts compressed responses, writes the compressed version of the content to the response. Otherwise it just writes the plain []byte to it.

func (*Context) RenderGzipJSON

func (c *Context) RenderGzipJSON(data interface{})

RenderGzipJSON takes a interface{} object and writes the JSON verion through RenderGzip.

func (*Context) RenderGzipXML

func (c *Context) RenderGzipXML(data interface{})

RenderGzipXML takes a interface{} object and writes the XML verion through RenderGzip.

func (*Context) RenderJSON

func (c *Context) RenderJSON(data interface{})

RenderJSON takes a interface{} object and writes the JSON encoded string of it.

func (*Context) RenderJSONIndent

func (c *Context) RenderJSONIndent(data interface{})

RenderJSONIndent is the indented (beauty) of RenderJSON

func (*Context) RenderXML

func (c *Context) RenderXML(data interface{})

RenderXML takes a interface{} object and writes the XML encoded string of it.

func (*Context) RenderXMLIndent

func (c *Context) RenderXMLIndent(data interface{})

RenderXMLIndent is the indented (beauty) of RenderXML

func (*Context) Status

func (c *Context) Status(code int)

Status sets the HTTP status code to be returned on the response.

type ContextData

type ContextData interface {
	// Get retrieves a data item by it's key name.
	Get(key string) (interface{}, error)

	// Set saves a data item under a key name.
	Set(key string, data interface{}) error

	// Del removes the data item and key name for a given key.
	Del(key string) error
}

ContextData interface represents a common get/set/del set of methods to handle data storage. Is designed to be used as the Data property of the Context obejct. The Data property is a free storage unit that apps using the framework can implement to their convenience to share context data during a request life. All methods returns an error status that different implementations can design to fulfill their needs.

type CustomError

type CustomError struct {
	HTTPCode  int    // HTTP status code to be used as this error response.
	ErrorCode int    // Internal YARF error code for further reference.
	ErrorMsg  string // YARF error message.
	ErrorBody string // Error content to be rendered to the client response.
}

CustomError is the standard error response format used through the framework. Implements Error and YError interfaces

func (*CustomError) Body

func (e *CustomError) Body() string

Body returns the error's content body, if needed, to be returned in the HTTP response.

func (*CustomError) Code

func (e *CustomError) Code() int

Code returns the error's HTTP code to be used in the response.

func (*CustomError) Error

func (e *CustomError) Error() string

Implements the error interface returning the ErrorMsg value of each error.

func (*CustomError) ID

func (e *CustomError) ID() int

ID returns the error's ID for further reference.

func (*CustomError) Msg

func (e *CustomError) Msg() string

Msg returns the error's message, used to implement the Error interface.

type GroupRoute

type GroupRoute struct {
	// contains filtered or unexported fields
}

GroupRoute stores routes grouped under a single url prefix.

func RouteGroup

func RouteGroup(url string) *GroupRoute

RouteGroup creates a new GroupRoute object and initializes it with the provided url prefix. The object implements Router interface to being able to handle groups as routes. Groups can be nested into each other, so it's possible to add a GroupRoute as a route inside another GroupRoute. Includes methods to work with middleware.

func (*GroupRoute) Add

func (g *GroupRoute) Add(url string, h ResourceHandler)

Add inserts a new resource with it's associated route into the group object.

func (*GroupRoute) AddGroup

func (g *GroupRoute) AddGroup(r *GroupRoute)

AddGroup inserts a GroupRoute into the routes list of the group object. This makes possible to nest groups.

func (*GroupRoute) Dispatch

func (g *GroupRoute) Dispatch(c *Context) (err error)

Dispatch loops through all routes inside the group and dispatch the one that matches the request. Outside the box, works exactly the same as route.Dispatch().

func (*GroupRoute) Insert

func (g *GroupRoute) Insert(m MiddlewareHandler)

Insert adds a MiddlewareHandler into the middleware list of the group object.

func (*GroupRoute) Match

func (g *GroupRoute) Match(url string, c *Context) bool

Match loops through all routes inside the group and find for one that matches the request. After a match is found, the route matching is stored into Context.groupDispatch to being able to dispatch it directly after a match without looping again. Outside the box, works exactly the same as route.Match()

type GroupRouter

type GroupRouter interface {
	Router
	Add(string, ResourceHandler)
	AddGroup(*GroupRoute)
	Insert(MiddlewareHandler)
}

GroupRouter interface adds methods to work with children routers

type MethodNotImplementedError

type MethodNotImplementedError struct {
	CustomError
}

MethodNotImplementedError is used to communicate that a specific HTTP method isn't implemented by a resource.

func ErrorMethodNotImplemented

func ErrorMethodNotImplemented() *MethodNotImplementedError

ErrorMethodNotImplemented creates MethodNotImplementedError

type Middleware

type Middleware struct{}

Middleware struct is the default implementation of a Middleware and does nothing. Users can either implement both methods or composite this struct into their own. Both methods needs to be present to satisfy the MiddlewareHandler interface.

func (*Middleware) End

func (m *Middleware) End(c *Context) error

End will be executed ALWAYS after every request, even if there were errors present.

func (*Middleware) PostDispatch

func (m *Middleware) PostDispatch(c *Context) error

PostDispatch includes code to be executed after every Resource request.

func (*Middleware) PreDispatch

func (m *Middleware) PreDispatch(c *Context) error

PreDispatch includes code to be executed before every Resource request.

type MiddlewareHandler

type MiddlewareHandler interface {
	PreDispatch(*Context) error
	PostDispatch(*Context) error
	End(*Context) error
}

MiddlewareHandler interface provides the methods for request filters that needs to run before, or after, every request Resource is executed.

type NotFoundError

type NotFoundError struct {
	CustomError
}

NotFoundError is the HTTP 404 error equivalent.

func ErrorNotFound

func ErrorNotFound() *NotFoundError

ErrorNotFound creates NotFoundError

type Params

type Params map[string]string

Params wraps a map[string]string and adds Get/Set/Del methods to work with it. Inspired on url.Values but simpler as it doesn't handles a map[string][]string

func (Params) Del

func (p Params) Del(key string)

Del deletes the values associated with key.

func (Params) Get

func (p Params) Get(key string) string

Get gets the first value associated with the given key. If there are no values associated with the key, Get returns the empty string.

func (Params) Set

func (p Params) Set(key, value string)

Set sets the key to value. It replaces any existing values.

type Resource

type Resource struct{}

The Resource type is the representation of each REST resource of the application. It implements the ResourceHandler interface and allows the developer to extend the methods needed. All resources being used by a YARF application have to composite this Resource struct.

func (*Resource) Connect

func (r *Resource) Connect(c *Context) error

Connect is the default HTTP CONNECT implementation. It returns a NotImplementedError

func (*Resource) Delete

func (r *Resource) Delete(c *Context) error

Delete is the default HTTP DELETE implementation. It returns a NotImplementedError

func (*Resource) Get

func (r *Resource) Get(c *Context) error

Get is the default HTTP GET implementation. It returns a NotImplementedError

func (*Resource) Head

func (r *Resource) Head(c *Context) error

Head is the default HTTP HEAD implementation. It returns a NotImplementedError

func (*Resource) Options

func (r *Resource) Options(c *Context) error

Options is the default HTTP OPTIONS implementation. It returns a NotImplementedError

func (*Resource) Patch

func (r *Resource) Patch(c *Context) error

Patch is the default HTTP PATCH implementation. It returns a NotImplementedError

func (*Resource) Post

func (r *Resource) Post(c *Context) error

Post is the default HTTP POST implementation. It returns a NotImplementedError

func (*Resource) Put

func (r *Resource) Put(c *Context) error

Put is the default HTTP PUT implementation. It returns a NotImplementedError

func (*Resource) Trace

func (r *Resource) Trace(c *Context) error

Trace is the default HTTP TRACE implementation. It returns a NotImplementedError

type ResourceHandler

type ResourceHandler interface {
	// HTTP methods
	Get(*Context) error
	Post(*Context) error
	Put(*Context) error
	Patch(*Context) error
	Delete(*Context) error
	Options(*Context) error
	Head(*Context) error
	Trace(*Context) error
	Connect(*Context) error
}

The ResourceHandler interface defines how Resources through the application have to be defined. Ideally, the developer will composite the Resource struct into their own resources, but it's possible to implement each one by their own.

type RouteCache

type RouteCache struct {
	// contains filtered or unexported fields
}

RouteCache stores previously matched and parsed routes

type Router

type Router interface {
	Match(string, *Context) bool
	Dispatch(*Context) error
}

Router interface provides the methods used to handle route and GroupRoute objects.

func Route

func Route(url string, h ResourceHandler) Router

Route returns a new route object initialized with the provided data. Params:

- url string 		// The route path to handle
- h	ResourceHandler	// The ResourceHandler object that will process the requests to the url.

type UnexpectedError

type UnexpectedError struct {
	CustomError
}

UnexpectedError is used when the origin of the error can't be discovered

func ErrorUnexpected

func ErrorUnexpected() *UnexpectedError

ErrorUnexpected creates UnexpectedError

type YError

type YError interface {
	Code() int    // HTTP response code for this error
	ID() int      // Error code ID.
	Msg() string  // Error description
	Body() string // Error body content to be returned to the client if needed.
}

YError is the interface used to handle error responses inside the framework.

type Yarf

type Yarf struct {
	// UseCache indicates if the route cache should be used.
	UseCache bool

	// Debug enables/disables the debug mode.
	// On debug mode, extra error information is sent to the client.
	Debug bool

	// PanicHandler can store a func() that will be defered by each request to be able to recover().
	// If you need to log, send information or do anything about a panic, this is your place.
	PanicHandler func()

	GroupRouter

	// Logger object will be used if present
	Logger *log.Logger

	// Follow defines a standard http.Handler implementation to follow if no route matches.
	Follow http.Handler

	// NotFound defines a function interface to execute when a NotFound (404) error is thrown.
	NotFound func(c *Context)
	// contains filtered or unexported fields
}

Yarf is the main entry point for the framework and it centralizes most of the functionality. All configuration actions are handled by this object.

func New

func New() *Yarf

New creates a new yarf and returns a pointer to it. Performs needed initializations

func (*Yarf) ServeHTTP

func (y *Yarf) ServeHTTP(res http.ResponseWriter, req *http.Request)

ServeHTTP Implements http.Handler interface into yarf. Initializes a Context object and handles middleware and route actions. If an error is returned by any of the actions, the flow is stopped and a response is sent. If no route matches, tries to forward the request to the Yarf.Follow (http.Handler type) property if set. Otherwise it returns a 404 response.

func (*Yarf) Start

func (y *Yarf) Start(address string)

Start initiates a new http yarf server and start listening. It's a shortcut for http.ListenAndServe(address, y)

func (*Yarf) StartTLS

func (y *Yarf) StartTLS(address, cert, key string)

StartTLS initiates a new http yarf server and starts listening to HTTPS requests. It is a shortcut for http.ListenAndServeTLS(address, cert, key, yarf)

Directories

Path Synopsis
examples
complete
The complete example implements all possible features from YARF.
The complete example implements all possible features from YARF.
static
static example package demonstrates how to easily implement a yarf handler that serves static files using the net/http package.
static example package demonstrates how to easily implement a yarf handler that serves static files using the net/http package.