Yet Another REST Framework

GoDoc Version Build Status Coverage Status Go Report Card Join the chat at https://gitter.im/jinzhu/gorm

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.

Comments
  • Data Races

    Data Races

    yarf appears to have several data races, some of which seem to be part of the framework's API.

    Note the documentation of http.Serve (Which is called by http.ListenAndServe among others.)(Emphasis mine):

    Serve accepts incoming connections on the Listener l, creating a new service goroutine for each. The service goroutines read requests and then call srv.Handler to reply to them.

    I created a new test on my fork that can surfaces data races in yarf.ServeHTTP, simulating the way Server behaves by spawning many goroutines to handle many requests. The output of go test -race with this test is at the bottom of this issue. The post on the Go Blog, Intoducing the Go Race Detector may be an insightful read.

    1. The first is a race on the cache itself. The first goroutine is reading from it at the same time as another is writing to it. As a stop-gap it could use a sync.RWMutex to serialize access to it. But since the current cache implementation is quite simplified, it might be better off removed from yarf completely and implemented at a different layer.
    2. The second race is on routeGroup.lastMatch. This could result in one goroutine overwriting lastMatch between another goroutine's write of lastMatch and call of Dispatch, causing the same Route to be dispatched to twice with two or more completely different contexts, and the other routes ignored completely. To fix this, I would put the last match in a new Dispatchee field of Context, and remove it from RouteGroup.
    3. The third race is on a Route's Context field (via embedded RequestContext). This means that two goroutines could call SetContext before either calls Dispatch, resulting in both routes getting the same context. Since this is part of the yarf's API, I doubt this could be fixed without breaking things. One change would be to change ResourceHandler http method handlers to take *Context as an argument instead of being passed underneath in the struct with RequestContext.

    This issue doesn't come with a pull request just because applying these changes would alter the API and I'd like your feedback first.

    ==================
    WARNING: DATA RACE
    Read by goroutine 39:
      runtime.mapaccess2_faststr()
          C:/sdk/Go/src/runtime/hashmap_fast.go:281 +0x0
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:83 +0x23a
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:131 +0xc1
    
    Previous write by goroutine 38:
      runtime.mapassign1()
          C:/sdk/Go/src/runtime/hashmap.go:411 +0x0
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:98 +0x564
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:131 +0xc1
    
    Goroutine 39 (running) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    
    Goroutine 38 (finished) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    ==================
    ==================
    WARNING: DATA RACE
    Read by goroutine 40:
      github.com/yarf-framework/yarf.(*routeGroup).Dispatch()
          go/src/github.com/yarf-framework/yarf/router.go:158 +0x60
      github.com/yarf-framework/yarf.(*yarf).dispatch()
          go/src/github.com/yarf-framework/yarf/yarf.go:135 +0x1bc
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:88 +0x30d
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:131 +0xc1
    
    Previous write by goroutine 38:
      github.com/yarf-framework/yarf.(*routeGroup).Match()
          go/src/github.com/yarf-framework/yarf/router.go:146 +0x2a5
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:95 +0x459
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:131 +0xc1
    
    Goroutine 40 (running) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    
    Goroutine 38 (finished) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    ==================
    ==================
    WARNING: DATA RACE
    Write by goroutine 39:
      github.com/yarf-framework/yarf.(*MockResource).SetContext()
          <autogenerated>:83 +0x64
      github.com/yarf-framework/yarf.(*route).Dispatch()
          go/src/github.com/yarf-framework/yarf/router.go:63 +0x81
      github.com/yarf-framework/yarf.(*routeGroup).Dispatch()
          go/src/github.com/yarf-framework/yarf/router.go:175 +0x2e5
      github.com/yarf-framework/yarf.(*yarf).dispatch()
          go/src/github.com/yarf-framework/yarf/yarf.go:135 +0x1bc
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:88 +0x30d
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:131 +0xc1
    
    Previous write by goroutine 38:
      github.com/yarf-framework/yarf.(*MockResource).SetContext()
          <autogenerated>:83 +0x64
      github.com/yarf-framework/yarf.(*route).Dispatch()
          go/src/github.com/yarf-framework/yarf/router.go:63 +0x81
      github.com/yarf-framework/yarf.(*routeGroup).Dispatch()
          go/src/github.com/yarf-framework/yarf/router.go:175 +0x2e5
      github.com/yarf-framework/yarf.(*yarf).dispatch()
          go/src/github.com/yarf-framework/yarf/yarf.go:135 +0x1bc
      github.com/yarf-framework/yarf.(*yarf).ServeHTTP()
          go/src/github.com/yarf-framework/yarf/yarf.go:102 +0x599
      github.com/yarf-framework/yarf.TestYarfParallel.func1()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:132 +0x12c
    
    Goroutine 39 (running) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    
    Goroutine 38 (finished) created at:
      github.com/yarf-framework/yarf.TestYarfParallel()
          go/src/github.com/yarf-framework/yarf/yarf_test.go:133 +0x4f9
      testing.tRunner()
          C:/sdk/Go/src/testing/testing.go:456 +0xe3
    ==================
    
  • Fix cache being written when UseCache=false

    Fix cache being written when UseCache=false

    yarf.ServeHTTP writes to the cache unconditionally. Specifically yarf.go:97. This pull request changes this to only write to the cache when UseCache = true.

  • can you explain the idea behind this

    can you explain the idea behind this

    Hi, I'm courious about this code

    
    type Hello struct {
        yarf.Resource
    }
    
    

    for me it's a bit verbos create this struct, compared to other frameworks inspired by ruby sinatra (similar to gorrilla routes) but I'm pretty sure than there is a great idea behind this, can you explain me why did you choose this implementation? thanks

  • Refactor yarf.go; fix critical routeGroup bug

    Refactor yarf.go; fix critical routeGroup bug

    The yarf struct was duplicating the functionality of routeGroup. The first commit in this pull request removes the duplication, and embeds a generic interface instead. Since the interface is embedded, the exposed methods don't change, but it gives us more flexibility. I also shuffled some of the applicable yarf tests into routeGroup tests. There is one extra level of indirection, but it doesn't affect the benchmarks much.

    Probably more important is a bug using Context.routeDispatch. Due to my meddling about with silly things such as 'race conditions' (#12), I suggested we put routeDispatch on the Context object instead of passing it through the route itself. Lo and behold, a bad data race turned into a worse logic error. If there are two routeGroups in the full path, the group at the base of the url overwrites routeDispatch, leading to an infinitely recursive dispatch.

    Luckily this logic was just deduplicated so the majority of the fix only needed to touch routeGroup. The change basically turns Context.routeDispatch into a []Route used like a stack. Every matched routeGroup pushes the found route to this slice. When dispatching, each route pops the last Route and dispatches it. It also adds a test to detect this.

    It looks like we need some more tests on Dispatch.

    (Normally I'd try to separate these, but they became somewhat interdependent.)

  • Benchmarks for Match; router.go cleanup

    Benchmarks for Match; router.go cleanup

    This pull request adds benchmarks for the route matching functions, and then improves on them while consolidating some of the repeated code in router.go into helper functions.

    This is the result of benchcmp between the two commits in this pull request.

    benchmark                           old ns/op     new ns/op     delta
    BenchmarkRouteMatch_short           352           296           -15.91%
    BenchmarkRouteMatch_long            1964          1845          -6.06%
    BenchmarkRouteMatch_emptyParts      4166          3596          -13.68%
    BenchmarkRouteGroupMatch_short      560           286           -48.93%
    BenchmarkRouteGroupMatch_nested     55565         51172         -7.91%
    
  • Test for & fix unmatched RouteGroup writing Params

    Test for & fix unmatched RouteGroup writing Params

    There's a bug in routeGroup.Match where it would write parameters to Context.Params before checking that the route fully matched, specifically it's children routes. This could lead to extra params that are not listed in the final url. This pull request fixes the bug.

    Also renamed prepareUrl to prepareURL to make golint happy.

  • url with tailling flash /hello/aa/ match route

    url with tailling flash /hello/aa/ match route "/hello/:name" should return 404 like net/http

    package main
    
    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
    }
    func main() {
    	// Create a new empty YARF server
    	y := yarf.New()
    	hello := new(Hello)
    	y.Add("/", hello)
    	y.Add("/hello/:name", hello)
    	y.Start(":8080")
    }
    
    

    $ curl http://localhost:8080/hello/xxx/ Hello, xxx

    package main
    import (
        "fmt"
        "net/http"
    )
    func main() {
        http.HandleFunc("/hello/xxx", func (w http.ResponseWriter, r *http.Request) {
            fmt.Fprintf(w, "Welcome to my website!")
        })
       http.ListenAndServe(":80", nil)
    }
    

    curl http://localhost:8080/hello/xxx/ Not found

skr: The lightweight and powerful web framework using the new way for Go.Another go the way.
skr: The lightweight and powerful web framework using the new way for Go.Another go the way.

skr Overview Introduction Documents Features Install Quickstart Releases Todo Pull Request Issues Thanks Introduction The lightweight and powerful web

Jan 11, 2022
A small and evil REST framework for Go

go-rest A small and evil REST framework for Go Reflection, Go structs, and JSON marshalling FTW! go get github.com/ungerik/go-rest import "github.com/

Dec 6, 2022
A REST framework for quickly writing resource based services in Golang.

What is Resoursea? A high productivity web framework for quickly writing resource based services fully implementing the REST architectural style. This

Sep 27, 2022
🍐 Elegant Golang REST API Framework
🍐 Elegant Golang REST API Framework

An Elegant Golang Web Framework Goyave is a progressive and accessible web application framework focused on REST APIs, aimed at making backend develop

Jan 9, 2023
REST API boilerplate built with go and clean architecture - Echo Framework

GO Boilerplate Prerequisite Install go-migrate for running migration https://github.com/golang-migrate/migrate App requires 2 database (postgreSQL an

Jan 2, 2023
Example Golang API backend rest implementation mini project Point Of Sale using Gin Framework and Gorm ORM Database.

Example Golang API backend rest implementation mini project Point Of Sale using Gin Framework and Gorm ORM Database.

Dec 23, 2022
REST API with Echo Framework from Go
REST API with Echo Framework from Go

REST API with Echo Framework from Go

Nov 16, 2021
REST api using fiber framework written in golang and using firebase ecosystem to authentication, storage and firestore as a db and use clean architecture as base
REST api using fiber framework written in golang and using firebase ecosystem to authentication, storage and firestore as a db and use clean architecture as base

Backend API Example FiberGo Framework Docs : https://github.com/gofiber Info This application using firebase ecosystem Firebase Auth Cloud Storage Fir

May 31, 2022
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 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

Jan 5, 2023
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

Nov 14, 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

Jul 14, 2022
laravel for golang,goal,fullstack framework,api framework
laravel for golang,goal,fullstack framework,api framework

laravel for golang,goal,fullstack framework,api framework

Feb 24, 2022
package for building REST-style Web Services using Go

go-restful package for building REST-style Web Services using Google Go Code examples using v3 REST asks developers to use HTTP methods explicitly and

Jan 1, 2023
Opinionated Go starter with gin for REST API, logrus for logging, viper for config with added graceful shutdown

go-gin-starter An opinionated starter for Go Backend projects using: gin-gonic/gin as the REST framework logrus for logging viper for configs Docker f

Dec 2, 2022
A Go REST API allowing me to send messages to myself, on my phone, according to some events.
A Go REST API allowing me to send messages to myself, on my phone, according to some events.

go-telegram-notifier go-telegram-notifier A Go REST API wrapping the official Telegram API and used to send myself notifications, on my phone, based o

Apr 27, 2022
Mattermost Posts via its REST API v4
Mattermost Posts via its REST API v4

A Go (golang) simple client for sending Mattermost posts via its REST API v4. This program makes use of the Go libraries http and url for interacting with a Mattermost server and Cobra coupled with Viper to implement the CLI interface.

Dec 1, 2022
A REST web-service sample project written in Golang using go-fiber, GORM and PostgreSQL

backend A REST web-service sample project written in Golang using go-fiber, GORM and PostgreSQL How to run Make sure you have Go installed (download).

Jan 1, 2023
Go WhatsApp REST API Implementation Using Fiber And Swagger
Go WhatsApp REST API Implementation Using Fiber And Swagger

Go WhatsApp REST API Implementation Using Fiber And Swagger Package cooljar/go-whatsapp-fiber Implements the WhatsApp Web API using Fiber web framewor

May 9, 2022