βββββββ βββ ββββββ ββββββββββ βββ
ββββββββββββ ββββββββββββ βββ ββββ
βββ ββββββ βββββββββ βββββββ
βββββ ββββββ βββββββββ βββββββ
βββββββββββββββββββββββββββββ βββ βββ
βββββββ βββββββ βββ βββββββ βββ βββ
Quick v0.0.1 π Fast & Minimal Web Framework
βββββββββββββββββββ βββββββββββββββββββββββββββββββ
π Host : http://127.0.0.1:0.0.0.0:8080
π Port : 0.0.0.0:8080
π Routes: 4
βββββββββββββββββββ βββββββββββββββββββββββββββββββ
π Quick is a flexible and extensible route manager for the Go language. Its goal is to be fast and high-performance, as well as being 100% compatible with net/http. Quick is a project in constant development and is open for collaboration, everyone is welcome to contribute. π
π‘ If youβre new to coding, Quick is a great opportunity to start learning how to work with Go. With its ease of use and features, you can create custom routes and expand your knowledge of the language.
π I hope you can participate and enjoy Enjoy! π
π The repository of examples of the Framework Quick Run Examples.
To access the documentation for each Quick Framework package, click on the links below:
Features | Has | Status | Completion |
---|---|---|---|
π£οΈ Route Manager | yes | π’ | 100% |
π Server Files Static | yes | π’ | 100% |
π Http Client | yes | π’ | 100% |
π€ Upload Files (multipart/form-data) | yes | π’ | 100% |
πͺ Route Group | yes | π’ | 100% |
π‘οΈ Middlewares | yes | π‘ | 50% |
β‘ HTTP/2 support | yes | π’ | 100% |
π Data binding for JSON, XML and form payload | yes | π’ | 100% |
π Regex support | yes | π‘ | 80% |
π Site | yes | π‘ | 90% |
π Docs | yes | π‘ | 40% |
Task | Progress |
---|---|
Develop MaxBodySize method Post | β 100% |
Develop MaxBodySize method Put | β 100% |
Develop Config in New(Config{}) not required | β 100% |
Create print function to not use fmt too much | β 100% |
Creation of own function for Concat String | β 100% |
Creation of benchmarking between the Stdout and fmt.Println | β 100% |
Develop Routes GET method | β 100% |
Develop Routes GET method by accepting Query String | β 100% |
Develop Routes GET method accepting Parameters | β 100% |
Develop Routes GET method accepting Query String and Parameters | β 100% |
Develop Routes GET method accepting regular expression | β 100% |
Develop Routes Method POST | β 100% |
Develop Routes POST method accepting JSON | β 100% |
Develop for METHOD POST the parse JSON | β 100% |
Develop for the POST METHOD functions to access byte or string from Parse | β 100% |
Develop for PUT METHOD | β 100% |
Develop for the PUT METHOD the JSON parse | β 100% |
Develop for the PUT METHOD the JSON parse | β 100% |
Develop for METHOD PUT functions to access byte or string from the Parse | β 100% |
Develop for DELETE METHOD | β 100% |
Develop method for ListenAndServe | β 100% |
Develop ServeHTTP support | β 100% |
Develop middleware support | β 100% |
Develop support for middleware compress | β 100% |
Develop support for middleware cors | β 100% |
Develop logger middleware support | β 100% |
Develop support for maxbody middlewares | β 100% |
Develop middleware support msgid | β 100% |
Develop middleware support msguuid | β 100% |
Develop support Cors | β 100% |
Develop Cient Get | β 100% |
Develop Cient Post support | β 100% |
Develop Cient Put support | β 100% |
Develop Cient support Delete | β 100% |
Task | Progress |
---|---|
Develop and relate to Listen the Config | β³ 42% |
Develops support for Uploads and Uploads Multiples | β 100% |
Develops support for JWT | β³ 10% |
Develop method to Facilitate ResponseWriter handling | β³ 80% |
Develop method to Facilitate the handling of the Request | β³ 80% |
Develop Standard of Unit Testing | β³ 90% |
Task | Progress |
---|---|
Documentation Tests Examples PKG Go | β³ 45% |
Test Coverage go test -cover | β³ 74.6% |
Regex feature coverage, but possibilities | π΄ 0% |
Develop for OPTIONS METHOD | β 100% |
Develop for CONNECT METHOD See more | π΄ 0% |
Develop method for ListenAndServeTLS (http2) | π΄ 0% |
Develop Static Files support | β 100% |
WebSocket Support | π΄ 0% |
Rate Limiter Support | π΄ 0% |
Template Engines | π΄ 0% |
Documentation Tests Examples PKG Go | β³ 45% |
Test coverage go test -cover | β³ 75.5% |
Coverage of Regex resources, but possibilities | π΄ 0% |
Develop for METHOD OPTIONS | β 100% |
Develop for CONNECT METHOD See more | π΄ 0% |
Develop method for ListenAndServeTLS (http2) | π΄ 0% |
Create a CLI (Command Line Interface) Quick. | π΄ 0% |
Archive | Coverage | Status |
---|---|---|
Ctx | π‘ 84.1% | π‘ |
Group | β 100.0% | π’ |
Http Status | π΄ 7.8% | π΄ |
Client | π’ 83.3% | π’ |
Mock | β 100.0% | π’ |
Concat | β 100.0% | π’ |
Log | π΄ 0.0% | π΄ |
π‘ 66.7% | π‘ | |
Qos | π΄ 0.0% | π΄ |
Rand | π΄ 0.0% | π΄ |
Compressa | π‘ 71.4% | π‘ |
Cors | π‘ 76.0% | π‘ |
Logger | β 100.0% | π’ |
Maxbody | β 100.0% | π’ |
Msgid | β 100.0% | π’ |
Msguuid | π’ 86.4% | π’ |
Quick | π‘ 79.5% | π‘ |
QuickTest | β 100.0% | π’ |
When using New, you can configure global parameters such as request body limits, read/write time-out and route capacity. Below is a custom configuration:
var ConfigDefault = Config{
BodyLimit: 4 * 1024 * 1024,
MaxBodySize: 4 * 1024 * 1024,
MaxHeaderBytes: 2 * 1024 * 1024,
RouteCapacity: 500,
MoreRequests: 500,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 2 * time.Second,
ReadHeaderTimeout: 1 * time.Second,
}
Check out the code below:
package main
import "github.com/jeffotoni/quick"
func main() {
// Initialize a new Quick instance
q := quick.New()
// Define a simple GET route at the root path
q.Get("/v1/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Status(200).SendString("Quick in action β€οΈ!")
})
/// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XGET -H "Content-Type:application/json" \
'localhost:8080/v1/user'
"Quick in action β€οΈ!"
The example below defines a GET/v1/customer/:param1/:param2 endpoint, where :param1 and :param2 are variables on the route that can receive dynamic values.
package main
import "github.com/jeffotoni/quick"
func main() {
// Initialize a new Quick instance
q := quick.New()
// Define a GET route with two dynamic parameters (:param1 and :param2)
q.Get("/v1/customer/:param1/:param2", func(c *quick.Ctx) error {
// Set response content type to JSON
c.Set("Content-Type", "application/json")
// Define a struct for the response format
type my struct {
Msg string `json:"msg"`
Key string `json:"key"`
Val string `json:"val"`
}
// Return a JSON response with extracted parameters
return c.Status(200).JSON(&my{
Msg: "Quick β€οΈ",
Key: c.Param("param1"),
Val: c.Param("param2"),
})
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XGET -H "Content-Type:application/json" \
'localhost:8080/v1/customer/val1/val2'
{
"msg":"Quick β€οΈ",
"key":"val1",
"val":"val2"
}
This example shows how to create a POST endpoint to receive and process a JSON request body. The code reads the data sent in the request body and converts it to a Go structure.
package main
import "github.com/jeffotoni/quick"
// Define a struct to map the expected JSON body
type My struct {
Name string `json:"name"`
Year int `json:"year"`
}
func main() {
// Initialize a new Quick instance
q := quick.New()
// Define a POST route to handle JSON request body
q.Post("/v1/user", func(c *quick.Ctx) error {
var my My
// Parse the request body into the 'my' struct
err := c.BodyParser(&my)
if err != nil {
return c.Status(400).SendString(err.Error())
}
// Return the received JSON data
return c.Status(200).String(c.BodyString())
// Alternative:
// return c.Status(200).JSON(&my)
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XPOST -H "Content-Type:application/json" \
'localhost:8080/v1/user' \
-d '{"name":"jeffotoni", "year":1990}'
{
"name":"jeffotoni",
"year":1990
}
Quick provides a simplified API for managing uploads, allowing you to easily retrieve and manipulate files.
Method | Description |
---|---|
c.FormFile("file") |
Returns a single file uploaded in the form. |
c.FormFiles("files") |
Returns a list of uploaded files (multiple uploads). |
c.FormFileLimit("10MB") |
Sets an upload limit (default is 1MB ). |
uploadedFile.FileName() |
Returns the file name. |
uploadedFile.Size() |
Returns the file size in bytes. |
uploadedFile.ContentType() |
Returns the MIME type of the file. |
uploadedFile.Bytes() |
Returns the bytes of the file. |
uploadedFile.Save("/path/") |
Saves the file to a specified directory. |
uploadedFile.Save("/path", "your-name-file") |
Saves the file with your name. |
uploadedFile.SaveAll("/path") |
Saves the file to a specified directory. |
Framework | FormFile() |
FormFiles() |
Dynamic Limit | Methods (FileName() , Size() ) |
Save() , SaveAll() Method |
---|---|---|---|---|---|
Quick | β Yes | β Yes | β Yes | β Yes | β Yes |
Fiber | β Yes | β Yes | β No | β No (uses FileHeader directly) |
β Yes |
Gin | β Yes | β Yes | β No | β No (uses FileHeader directly) |
β No |
Echo | β Yes | β No | β No | β No | β No |
net/http | β Yes | β No | β No | β No | β No |
This example shows how to create a file upload endpoint. It allows users to send a single file via POST to the/upload route.
package main
import (
"fmt"
"github.com/jeffotoni/quick"
)
// Define a struct for error messages
type Msg struct {
Msg string `json:"msg"`
Error string `json:"error"`
}
func main() {
// Initialize a new Quick instance
q := quick.New()
// Define a route for file upload
q.Post("/upload", func(c *quick.Ctx) error {
// set limit upload
c.FormFileLimit("10MB")
// Retrieve the uploaded file
uploadedFile, err := c.FormFile("file")
if err != nil {
return c.Status(400).JSON(Msg{
Msg: "Upload error",
Error: err.Error(),
})
}
// Print file details
fmt.Println("Name:", uploadedFile.FileName())
fmt.Println("Size:", uploadedFile.Size())
fmt.Println("MIME Type:", uploadedFile.ContentType())
// Save the file (optional)
// uploadedFile.Save("/tmp/uploads")
// Return JSON response with file details
// Alternative:
//return c.Status(200).JSONIN(uploadedFile)
return c.Status(200).JSON(map[string]interface{}{
"name": uploadedFile.FileName(),
"size": uploadedFile.Size(),
"type": uploadedFile.ContentType(),
})
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X POST http://localhost:8080/upload -F "file=quick.txt"
{
"name":"quick.txt",
"size":1109,
"type":"text/plain; charset=utf-8"
}
This example allows users to send multiple files via POST to the/upload-multiple route
package main
import (
"fmt"
"github.com/jeffotoni/quick"
)
// Define a struct for error messages
type Msg struct {
Msg string `json:"msg"`
Error string `json:"error"`
}
func main() {
// Initialize a new Quick instance
q := quick.New()
q.Post("/upload-multiple", func(c *quick.Ctx) error {
// set limit upload
c.FormFileLimit("10MB")
// recebereceiving files
files, err := c.FormFiles("files")
if err != nil {
return c.Status(400).JSON(Msg{
Msg: "Upload error",
Error: err.Error(),
})
}
// listing all files
for _, file := range files {
fmt.Println("Name:", file.FileName())
fmt.Println("Size:", file.Size())
fmt.Println("Type MINE:", file.ContentType())
fmt.Println("Bytes:", file.Bytes())
}
// optional
// files.SaveAll("/my-dir/uploads")
// Alternative:
//return c.Status(200).JSONIN(files)
return c.Status(200).JSON("Upload successfully completed")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -X POST http://localhost:8080/upload-multiple \
-F "files=@image1.jpg" -F "files=@document.pdf"
Upload successfully completed
package main
import "github.com/jeffotoni/quick"
// Define a struct to map the expected JSON body
type My struct {
Name string `json:"name"`
Year int `json:"year"`
}
func main() {
// Initialize a new Quick instance
q := quick.New()
// Define a POST route to handle JSON request body
q.Post("/v2/user", func(c *quick.Ctx) error {
var my My
// Parse the request body into the 'my' struct
err := c.Bind(&my)
if err != nil {
// Return a 400 status if JSON parsing fails
return c.Status(400).SendString(err.Error())
}
// Return the received JSON data
return c.Status(200).JSON(&my)
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XPOST -H "Content-Type:application/json" \
'localhost:8080/v2/user' \
-d '{"name":"Marcos", "year":1990}'
{
"name":"Marcos",
"year":1990
}
Using the Cors middleware, making your call in the default way, which is:
var ConfigDefault = Config{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposedHeaders: []string{"Content-Length"},
AllowCredentials: false,
MaxAge: 600,
Debug: false,
}
Check out the code below:
package main
import (
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/cors"
)
func main() {
// Initialize a new Quick instance
q := quick.New()
// Use the CORS middleware to allow cross-origin requests
q.Use(cors.New())
// Define a GET route at /v1/user
q.Get("/v1/user", func(c *quick.Ctx) error {
// Set the response content type to JSON
c.Set("Content-Type", "application/json")
// Return a response with status 200 and a message
return c.Status(200).SendString("Quick in action com Corsβ€οΈ!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XGET -H "Content-Type:application/json" \
'http://localhost:8080/v1/user'
Quick in action com Corsβ€οΈ!
This example demonstrates how to start a Quick server with a custom configuration.
package main
import "github.com/jeffotoni/quick"
func main() {
// Create a new Quick server instance with custom configuration
q := quick.New(quick.Config{
MaxBodySize: 5 * 1024 * 1024, // Set max request body size to 5MB
})
// Define a GET route that returns a JSON response
q.Get("/v1/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json") // Set response content type
return c.Status(200).SendString("Quick in action com Corsβ€οΈ!") // Return response
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -XGET -H "Content-Type:application/json" \
'http://localhost:8080/v1/user'
Quick in action com Corsβ€οΈ!
This example demonstrates how to group routes using quick. Group(), making the code more organized
package main
import "github.com/jeffotoni/quick"
func main() {
// Create a new Quick server instance with custom configuration
q := quick.New(quick.Config{
MaxBodySize: 5 * 1024 * 1024,
})
// Group for /v1 routes
v1 := q.Group("/v1")
// Define GET and POST routes for /v1/user
v1.Get("/user", func(c *quick.Ctx) error {
return c.Status(200).SendString("[GET] [GROUP] /v1/user ok!!!")
})
v1.Post("/user", func(c *quick.Ctx) error {
return c.Status(200).SendString("[POST] [GROUP] /v1/user ok!!!")
})
// Group for /v2 routes
v2 := q.Group("/v2")
// Define GET and POST routes for /v2/user
v2.Get("/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Status(200).SendString("Quick in action com [GET] /v2/user β€οΈ!")
})
v2.Post("/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Status(200).SendString("Quick in action com [POST] /v2/user β€οΈ!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
1οΈβ£ GET /v1/user
$ curl -i -X GET http://localhost:8080/v1/user
[GET] [GROUP] /v1/user ok!!!
2οΈβ£ POST /v1/user
$ curl -i -X POST http://localhost:8080/v1/user
[POST] [GROUP] /v1/user ok!!!
3οΈβ£ GET /v2/user
$ curl -i -X GET http://localhost:8080/v2/user
Quick in action com [GET] /v2/user β€οΈ!
4οΈβ£ POST /v2/user
$ curl -i -X POST http://localhost:8080/v2/user
Quick in action com [POST] /v2/user β€οΈ!
This example demonstrates how to unit test routes in Quick using QuickTest(). It simulates HTTP requests and verifies if the response matches the expected output
package main
import (
"io"
"strings"
"testing"
"github.com/jeffotoni/quick"
)
func TestQuickExample(t *testing.T) {
// Here is a handler function Mock
testSuccessMockHandler := func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
b, _ := io.ReadAll(c.Request.Body)
resp := `"data":` + string(b)
return c.Byte([]byte(resp))
}
// Initialize Quick instance for testing
q := quick.New()
// Define test routes
q.Post("/v1/user", testSuccessMockHandler)
q.Post("/v1/user/:p1", testSuccessMockHandler)
// Expected response data
wantOutData := `"data":{"name":"jeff", "age":35}`
reqBody := []byte(`{"name":"jeff", "age":35}`)
reqHeaders := map[string]string{"Content-Type": "application/json"}
// Perform test request
data, err := q.QuickTest("POST", "/v1/user", reqHeaders, reqBody)
if err != nil {
t.Errorf("error: %v", err)
return
}
// Compare expected and actual response
s := strings.TrimSpace(data.BodyStr())
if s != wantOutData {
t.Errorf("Expected %s but got %s", wantOutData, s)
return
}
// Log test results
t.Logf("\nOutputBodyString -> %v", data.BodyStr())
t.Logf("\nStatusCode -> %d", data.StatusCode())
t.Logf("\nOutputBody -> %v", string(data.Body()))
t.Logf("\nResponse -> %v", data.Response())
}
This example allows access only when the ID is numeric ([0-9]+).
package main
import (
"github.com/jeffotoni/quick"
)
func main() {
// Initialize a new Quick instance
q := quick.New()
// Route that accepts only numeric IDs (using regex [0-9]+)
q.Get("/users/{id:[0-9]+}", func(c *quick.Ctx) error {
id := c.Param("id")
return c.JSON(map[string]string{
"message": "User found",
"user_id": id,
})
})
// Start the server on port 8080
q.Listen(":8080")
}
$ curl -i -X GET http://localhost:8080/users/123
{
"message":"User found",
"user_id":"123"
}
This example ensures that only lowercase letters ([a-z]+) are accepted in the slug
package main
import (
"github.com/jeffotoni/quick"
)
func main() {
// Initialize a new Quick instance
q := quick.New()
// Route that accepts only lowercase slugs (words with lowercase letters)
q.Get("/profile/{slug:[a-z]+}", func(c *quick.Ctx) error {
slug := c.Param("slug")
return c.JSON(map[string]string{
"message": "Profile found",
"profile": slug,
})
})
// Start the server on port 8080
q.Listen(":8080")
}
$ curl -i -X GET http://localhost:8080/profile/johndoe
{
"message":"Profile found",
"profile":"johndoe"
}
package main
import (
"github.com/jeffotoni/quick"
)
func main() {
// Initialize a new Quick instance
q := quick.New()
// Route that accepts an API version (v1, v2, etc.) and a numeric user ID
q.Get("/api/{version:v[0-9]+}/users/{id:[0-9]+}", func(c *quick.Ctx) error {
version := c.Param("version")
id := c.Param("id")
return c.JSON(map[string]string{
"message": "API Versioned User",
"version": version,
"user_id": id,
})
})
// Start the server on port 8080
q.Listen(":8080")
}
$ curl -i -X GET http://localhost:8080/api/v1/users/123
{
"message":"API Versioned User",
"user_id":"123",
"version":"v1"
}
Basic Authentication (Basic Auth) is a simple authentication mechanism defined in RFC 7617.
It is commonly used for HTTP-based authentication, allowing clients to provide credentials (username and password) in the request header.
1οΈβ£ The client encodes the username and password in Base64:
2οΈβ£ The encoded credentials are sent in the Authorization
header:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
3οΈβ£ The server decodes and verifies the credentials before granting access.
- π Not encrypted β Basic Auth only encodes credentials in Base64, but does not encrypt them.
- π Use over HTTPS β Always use Basic Auth with TLS/SSL (HTTPS) to prevent credentials from being exposed.
- π Consider alternatives β For stronger security, prefer OAuth2, JWT, or API keys. Basic Auth is suitable for simple use cases, but for production applications, stronger authentication mechanisms are recommended. π
This example sets up Basic Authentication using environment variables to store the credentials securely. the routes below are affected, to isolate the route use group to apply only to routes in the group.
package main
import (
"log"
"os"
"github.com/jeffotoni/quick"
middleware "github.com/jeffotoni/quick/middleware/basicauth"
)
// Environment variables for authentication
// export USER=admin
// export PASSWORD=1234
var (
// Retrieve the username and password from environment variables
User = os.Getenv("USER")
Password = os.Getenv("PASSORD")
)
func main() {
// Initialize a new Quick instance
q := quick.New()
// Apply Basic Authentication middleware
q.Use(middleware.BasicAuth(User, Password))
// Define a protected route
q.Get("/protected", func(c *quick.Ctx) error {
// Set the response content type to JSON
c.Set("Content-Type", "application/json")
// Return a success message
return c.SendString("You have accessed a protected route!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X GET http://localhost:8080/api/v1/users/123
You have accessed a protected route!
This example uses the built-in BasicAuth middleware provided by Quick, offering a simple authentication setup.
package main
import (
"log"
"github.com/jeffotoni/quick"
middleware "github.com/jeffotoni/quick/middleware/basicauth"
)
func main() {
//starting Quick
q := quick.New()
// calling middleware
q.Use(middleware.BasicAuth("admin", "1234"))
// everything below Use will apply the middleware
q.Get("/protected", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.SendString("You have accessed a protected route!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X GET 'http://localhost:8080/protected' \
--header 'Authorization: Basic YWRtaW46MTIzNA=='
You have accessed a protected route!
This example shows how to apply Basic Authentication to a specific group of routes using Quick's Group functionality. When we use group we can isolate the middleware, this works for any middleware in quick.
package main
import (
"log"
"github.com/jeffotoni/quick"
middleware "github.com/jeffotoni/quick/middleware/basicauth"
)
func main() {
//starting Quick
q := quick.New()
// using group to isolate routes and middlewares
gr := q.Group("/")
// middleware BasicAuth
gr.Use(middleware.BasicAuth("admin", "1234"))
// route public
q.Get("/v1/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.SendString("Public quick route")
})
// protected route
gr.Get("/protected", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.SendString("You have accessed a protected route!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X GET http://localhost:8080/v1/user
Public quick route
This example shows a custom implementation of Basic Authentication without using any middleware. It manually verifies user credentials and applies authentication to protected routes.
In quick you are allowed to make your own custom implementation directly in q.Use(..), that is, you will be able to implement it directly if you wish.
package main
import (
"encoding/base64"
"log"
"net/http"
"strings"
"github.com/jeffotoni/quick"
)
func main() {
//starting Quick
q := quick.New()
// implementing middleware directly in Use
q.Use(func(next http.Handler) http.Handler {
// credentials
username := "admin"
password := "1234"
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Check if it starts with "Basic"
if !strings.HasPrefix(authHeader, "Basic ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Decode credentials
payload, err := base64.StdEncoding.DecodeString(authHeader[len("Basic "):])
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
creds := strings.SplitN(string(payload), ":", 2)
if len(creds) != 2 || creds[0] != username || creds[1] != password {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
})
q.Get("/protected", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.SendString("You have accessed a protected route!")
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -u admin:1234 -X GET http://localhost:8080/protected
You have accessed a protected route!
A Static File Server is an essential feature in web frameworks, enabling the efficient serving of static content such as HTML, CSS, JavaScript, images, and other assets.
It is particularly useful for:
- β Hosting front-end applications
- β Providing downloadable files
- β Serving resources directly from the backend
- 1οΈβ£ The server listens for HTTP requests targeting static file paths.
- 2οΈβ£ If the requested file exists in the configured directory, the server reads and returns it as a response.
- 3οΈβ£ MIME types are automatically determined based on the file extension for correct rendering.
- π Efficient Handling β Serves files directly without additional processing.
- π― MIME Type Detection β Automatically identifies file types for proper rendering.
- β‘ Caching Support β Can be configured to improve performance via HTTP headers.
- π Optional Directory Listing β Allows browsing available static files (if enabled).
- π Restrict Access β Prevent exposure of sensitive files like
.env
,.git
, or configuration files. - π CORS Policies β Configure Cross-Origin Resource Sharing (CORS) to control file access.
- π‘ Content Security Policy (CSP) β Helps mitigate XSS (Cross-Site Scripting) risks.
By properly configuring your static file server, you can ensure fast, efficient, and secure delivery of resources! ππ₯
This example sets up a basic web server that serves static files, such as HTML, CSS, or JavaScript.
package main
import "github.com/jeffotoni/quick"
func main() {
//starting Quick
q := quick.New()
// Static Files Setup
q.Static("/static", "./static")
// Route Definition
q.Get("/", func(c *quick.Ctx) error {
c.File("./static/index.html")
return nil
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X GET http://localhost:8080/
File Server Go example html
- Static assets (HTML, CSS, JS, images, etc.) are compiled directly into the binary at compile time, using the Go package
embed
. - The application serves these files from memory, eliminating the need to access the disk.
- This removes external dependencies, making the deployment simpler and more efficient.
- β Portability - The binary contains everything you need, no extra files.
- β Performance - File access is faster because files are already loaded in memory.
- β Security - Reduces exposure to attacks because the file system does not need to be accessible.
The function q. Static()
already handles the complexity of serving embedded files. Just call it with embed.FS
.
The example shows how to serve static files with Quick and embed.FS
package main
import (
"embed"
"github.com/jeffotoni/quick"
)
//go:embed static
var staticFiles embed.FS
func main() {
// Server Initialization
q := quick.New()
// Static Files Setup (serves files automatically from embed.FS)
q.Static("/static", staticFiles)
// Defines a route that serves the HTML index file
q.Get("/", func(c *quick.Ctx) error {
c.File("./static/index.html")
return nil
})
// Start the server on port 8080
q.Listen("0.0.0.0:8080")
}
$ curl -i -X GET http://localhost:8080/
File Server Go example html
quick-example
βββ main.go
βββ static/
β βββ index.html
β βββ style.css
β βββ script.js
The HTTP Client package in Quick provides a simple and flexible way to make HTTP requests, supporting GET, POST, PUT, and DELETE operations. π
It is designed to handle different types of request bodies and parse responses easily.
- β Easy-to-Use β Simplified functions for common HTTP requests.
- β Highly Customizable β Supports headers, authentication, and transport settings.
- β Flexible Body Parsing β Works with JSON, plain text, and custom io.Reader types.
- β Automatic JSON Handling β No need to manually marshal/unmarshal JSON.
- πΉ Convenience Functions β Use
Get
,Post
,Put
, andDelete
to make quick requests with a default client. - πΉ Customizable Requests β Easily add headers, authentication, and request settings.
- πΉ Automatic JSON Processing β Seamless encoding and decoding of JSON data.
- πΉ Flexible Request Body β Send data as JSON, plain text, or any
io.Reader
.
The Client
struct represents a configurable HTTP client with advanced features:
var ClientDefault = Client{
Ctx: context.Background(),
ClientHTTP: httpGoClient{},
Headers: map[string]string{"Content-Type": "application/json"},
EnableLogger: true,
Logger: slog.Default(),
}
Check out the code below:
A GET request is used to retrieve data from a server.
package main
import (
"fmt"
"log"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new HTTP client
httpClient := client.New()
// Making a GET request to fetch a list of users
resp, err := httpClient.Get("https://reqres.in/api/users")
if err != nil {
log.Fatal(err)
}
// Alternative:
//fmt.Println("GET response:", string(resp.Body))
// Parse JSON response
var result map[string]interface{}
if err := json.Unmarshal(resp.Body, &result); err != nil {
log.Fatal("Error decoding response:", err)
}
// Extract first user
users := result["data"].([]interface{})
firstUser := users[0].(map[string]interface{})
// Print only first user
fmt.Printf("Id: %v\n", firstUser["id"])
fmt.Printf("Name: %v %v\n", firstUser["first_name"], firstUser["last_name"])
fmt.Printf("Email: %v\n", firstUser["email"])
fmt.Printf("Avatar: %v\n", firstUser["avatar"])
}
Id: 1
Name: George Bluth
Email: george.bluth@reqres.in
Avatar: https://reqres.in/img/faces/1-image.jpg
A POST request is used to send data to a server, often for creating new resources.
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new HTTP client
httpClient := client.New()
// Define a struct for the request body
data := struct {
Name string `json:"name"`
}{
Name: "Emma",
}
// Making a POST request with JSON data
resp, err := httpClient.Post("https://reqres.in/api/users", data)
if err != nil {
log.Fatal("Error making POST request:", err)
}
// Parse JSON response
var result map[string]interface{}
if err := json.Unmarshal(resp.Body, &result); err != nil {
log.Fatal("Error decoding response:", err)
}
// Alternative:
//fmt.Println("POST response:", result)
// Print formatted response
fmt.Println("Id:", result["id"])
fmt.Println("Created_At:", result["createdAt"])
}
Id: 322
Created_At: 2025-03-14T14:48:24.305Z
A PUT request is used to update an existing resource.
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new HTTP client
httpClient := client.New()
// Define a struct with updated user data
data := struct {
Name string `json:"name"`
}{
Name: "Jeff",
}
// PUT request to ReqRes API
resp, err := httpClient.Put("https://reqres.in/api/users/2", data)
if err != nil {
log.Fatal("Error making PUT request:", err)
}
// Parse JSON response
var result map[string]interface{}
if err := json.Unmarshal(resp.Body, &result); err != nil {
log.Fatal("Error decoding response:", err)
}
// Alternative: Print the HTTP status and response body
// fmt.Println("HTTP Status Code:", resp.StatusCode)
// fmt.Println("Raw Response Body:", string(resp.Body))
// Print formatted response
fmt.Println("Updated_At:", result["updatedAt"])
}
Updated_At: 2025-03-14T14:56:35.202Z
A DELETE request is used to remove a resource from the server.
package main
import (
"fmt"
"log"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new HTTP client
httpClient := client.New()
// DELETE request to ReqRes API
resp, err := httpClient.Delete("https://reqres.in/api/users/2")
if err != nil {
log.Fatal("Error making request:", err)
}
// Print the HTTP status to confirm deletion
fmt.Println("Status Code:", resp.StatusCode)
// Since DELETE usually returns no content, we check if it's empty
if len(resp.Body) > 0 {
fmt.Println("Raw Response Body:", string(resp.Body))
} else {
fmt.Println("Response Body is empty (expected for 204 No Content)")
}
}
Status Code: 204
Response Body is empty (expected for 204 No Content)
Qtest is an advanced HTTP testing function designed to simplify route validation within the Quick framework. It enables seamless testing of simulated HTTP requests using httptest
, supporting:
- Custom HTTP methods (
GET
,POST
,PUT
,DELETE
, etc.). - β Custom headers.
- β Query parameters.
- β Request body.
- β Cookies.
- β Built-in validation methods for status codes, headers, and response bodies.
The Qtest
function takes a QuickTestOptions
struct containing request parameters, executes the request, and returns a QtestReturn
object, which provides methods for analyzing and validating the result.
func TestQTest_Options_POST(t *testing.T) {
// start Quick
q := New()
// Define the POST route
q.Post("/v1/user/api", func(c *Ctx) error {
c.Set("Content-Type", "application/json") // Simplified header setting
return c.Status(StatusOK).String(`{"message":"Success"}`)
})
// Configure test parameters
opts := QuickTestOptions{
Method: "POST",
URI: "/v1/user/api",
QueryParams: map[string]string{
"param1": "value1",
"param2": "value2",
},
Body: []byte(`{"key":"value"}`),
Headers: map[string]string{
"Content-Type": "application/json",
},
Cookies: []*http.Cookie{
{Name: "session", Value: "abc123"},
},
LogDetails: true, // Enables detailed logging
}
// Execute test
result, err := q.Qtest(opts)
if err != nil {
t.Fatalf("Error in Qtest: %v", err)
}
// Validations
if err := result.AssertStatus(StatusOK); err != nil {
t.Errorf("Status assertion failed: %v", err)
}
if err := result.AssertHeader("Content-Type", "application/json"); err != nil {
t.Errorf("Header assertion failed: %v", err)
}
if err := result.AssertBodyContains("Success"); err != nil {
t.Errorf("Body assertion failed: %v", err)
}
}
Function | Description |
---|---|
Qtest(opts QuickTestOptions) |
Executes an HTTP test request |
AssertStatus(expected int) |
Asserts expected HTTP status code |
AssertHeader(key, value string) |
Checks response header value |
AssertBodyContains(substr string) |
Verifies if body contains a string |
π Check out the full documentation: Qtest - Quick
The Quick HTTP Client now includes built-in retry and failover support, allowing for more resilient and reliable HTTP requests. These features are essential for handling transient failures, network instability, and service downtime efficiently.
- π Automatic Retries: Retries failed requests based on configurable rules.
- β³ Exponential Backoff: Gradually increases the delay between retry attempts.
- π‘ Status-Based Retries: Retries only on specified HTTP status codes (e.g.,
500
,502
,503
). - π Failover Mechanism: Switches to predefined backup URLs if the primary request fails.
- π Logging Support: Enables detailed logs for debugging retry behavior.
The retry mechanism automatically resends requests when they fail, with configurable options to:
- Limit the number of retries to avoid excessive attempts.
- Introduce backoff delays to prevent overwhelming the server.
- Retry only on specific HTTP status codes (e.g.,
500
,502
,503
).
The failover system ensures high availability by redirecting failed requests to predefined backup URLs, reducing downtime and improving system resilience.
These options allow fine-grained control over retry and failover behavior:
Option | Description π |
---|---|
MaxRetries | Sets the maximum number of retry attempts before failure. |
Delay | Defines the initial delay before retrying a request. |
UseBackoff | Enables exponential backoff, increasing delay dynamically after each retry. |
Statuses | List of HTTP status codes (e.g., 500 , 502 , 503 ) that trigger a retry. |
FailoverURLs | List of backup URLs used if the primary request repeatedly fails. |
EnableLog | Enables detailed logging for debugging retry behavior. |
The Quick HTTP Client provides built-in support for retrying failed requests and switching to failover URLs when necessary.
You can configure these behaviors using the WithRetry
option, which accepts a RetryConfig
struct.
The following example shows how to create a Quick client with retry and failover mechanisms.
// Creating a Quick client using a custom HTTP client and retry settings.
cClient := client.New(
client.WithCustomHTTPClient(customHTTPClient),
client.WithContext(context.Background()),
client.WithHeaders(map[string]string{
"Content-Type": "application/json",
}),
client.WithRetry(client.RetryConfig{
MaxRetries: 3,
Delay: 2 * time.Second,
UseBackoff: true,
Statuses: []int{500, 502, 503, 504},
FailoverURLs: []string{"http://hosterror", "https://httpbin.org/post"},
EnableLog: true,
}),
)
This example demonstrates retrying a request with an increasing delay (backoff) when encountering errors.
package main
import (
"fmt"
"log"
"time"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new Quick HTTP client with retry settings
cClient := client.New(
client.WithRetry(
client.RetryConfig{
MaxRetries: 3,
Delay: 1 * time.Second,
UseBackoff: true,
Statuses: []int{500, 502, 503},
FailoverURLs: []string{"http://backup1.com/resource", "https://httpbin_error.org/get", "https://httpbin.org/get"},
EnableLog: true,
}),
)
// Send a GET request to the specified URL
resp, err := cClient.Get("https://httpbin_error.org/get")
if err != nil {
log.Fatal("GET request failed:", err)
}
// Print the response body
fmt.Println("GET Response:", string(resp.Body))
}
{"time":"2025-03-14T14:27:02.069237664-03:00","level":"WARN","msg":"Retrying request","url":"https://httpbin_error.org/get","method":"GET","attempt":1,"failover":1}
{"time":"2025-03-14T14:27:13.076907091-03:00","level":"WARN","msg":"Retrying request","url":"http://backup1.com/resource","method":"GET","attempt":2,"failover":2}
{"time":"2025-03-14T14:27:15.258544931-03:00","level":"WARN","msg":"Retrying request","url":"https://httpbin_error.org/get","method":"GET","attempt":3,"failover":3}
GET Response: {
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Host": "httpbin_error.org",
"User-Agent": "Go-http-client/1.1",
"X-Amzn-Trace-Id": "Root=1-67d466f8-1aafed0512167ac32426bc9f"
},
"origin": "179.216.110.129",
"url": "https://httpbin_error.org/get"
}
- The retry mechanism is triggered because
MaxRetries: 3
allows the request to be retried up to three times. - The wait time between attempts automatically increases due to
UseBackoff: true
. - A retry only occurs if the response contains an HTTP error listed in
Statuses: []int{500, 502, 503}
.
- If all retry attempts on the primary URL fail, the client will try the alternative URLs listed in
FailoverURLs
. - In this example, if
https://httpbin.org/status/500
keeps failing, it will switch tohttps://httpbin.org/get
.
The Quick HTTP Client now supports PostForm
, making it easier to send form-encoded data (application/x-www-form-urlencoded
).
This feature is particularly useful for:
β
Authentication requests
β
Submitting data to web services
β
Integrations with legacy systems that do not accept JSON
π Feature | π‘ Benefit |
---|---|
π Optimized for Forms | Makes it easy to send form-encoded data (application/x-www-form-urlencoded ). |
βοΈ Automatic Encoding | Converts url.Values into a valid form submission format. |
π Header Management | Automatically sets Content-Type: application/x-www-form-urlencoded . |
π Consistent API | Follows the same design as Post , Get , Put , ensuring ease of use. |
π Better Compatibility | Works seamlessly with APIs that do not accept JSON payloads. |
The PostForm method encodes form parameters, adds necessary headers, and sends an HTTP POST request to the specified URL. It is specifically designed for APIs and web services that do not accept JSON payloads but require form-encoded data.
The following example demonstrates how to send form-encoded data using Quick PostForm:
package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"time"
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Initialize Quick framework
q := quick.New()
// Define a POST route to handle form data submission
q.Post("/postform", func(c *quick.Ctx) error {
// Retrieve form values from the request
form := c.FormValues()
// Return the received form data as JSON response
return c.JSON(map[string]any{
"message": "Received form data",
"data": form,
})
})
// Start the Quick server in a separate goroutine
go func() {
fmt.Println("Quick server running at http://localhost:3000")
if err := q.Listen(":3000"); err != nil {
log.Fatalf("Failed to start Quick server: %v", err)
}
}()
time.Sleep(2 * time.Second)
// Create an HTTP client before calling PostForm
cClient := client.New(
client.WithTimeout(5*time.Second),
client.WithHeaders(map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
}),
)
// Declare form data (key-value pairs)
formData := url.Values{}
formData.Set("username", "quick_user")
formData.Set("password", "supersecret")
// Send a POST request with form data
resp, err := cClient.PostForm("http://localhost:3000/postform", formData)
if err != nil {
log.Fatalf("PostForm request failed: %v", err)
}
// Unmarshal the JSON response from the server
var result map[string]any
if err := json.Unmarshal(resp.Body, &result); err != nil {
log.Fatal("Failed to parse JSON response:", err)
}
// Print the formatted JSON response
// Alternative:fmt.Println("POST Response:", result)
// Print the formatted JSON response
formattedResponse, err := json.MarshalIndent(result, "", " ")
if err != nil {
log.Fatal("Failed to format JSON response:", err)
}
fmt.Println("POST Response:")
fmt.Println(string(formattedResponse))
}
POST Response:
{
"data": {
"password": [
"supersecret"
],
"username": [
"quick_user"
]
},
"message": "Received form data"
}
The Transport
setting in the Quick HTTP Client manages the network layer, ensuring efficient, secure, and reliable HTTP communications.
It provides fine-grained control over connection management, security settings, and protocol optimizations for both development and production environments.
βοΈ Setting | π Description |
---|---|
π Proxy Settings | Handles proxy servers using system environment settings for automatic configuration. |
π TLS Configuration | Controls security settings, such as TLS version and certificate verification. InsecureSkipVerify can be enabled for development to bypass SSL verification. |
π‘ Connection Management | Optimizes resource usage with settings like MaxIdleConns , MaxConnsPerHost , and MaxIdleConnsPerHost , improving scalability. |
π Persistent Connections | Enables or disables Keep-Alives, reducing connection setup time and improving performance. |
β‘ HTTP/2 Support | Enables HTTP/2 for faster, more efficient communication when supported by the server. |
This code example showcases the setup of an HTTP client capable of handling network interruptions and server failures gracefully. It features custom transport configurations, including enhanced security settings, connection management, and a robust failover mechanism. Such a setup ensures that the application remains resilient and responsive under various network conditions.
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
"github.com/jeffotoni/quick/http/client"
)
func main() {
customTransport := &http.Transport{
// Uses system proxy settings if available.
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
// Allows insecure TLS connections .
InsecureSkipVerify: true,
// Enforces a minimum TLS version for security.
MinVersion: tls.VersionTLS12,
},
// Maximum number of idle connections across all hosts.
MaxIdleConns: 50,
// Maximum simultaneous connections per host.
MaxConnsPerHost: 30,
// Maximum number of idle connections per host.
MaxIdleConnsPerHost: 10,
// Enables persistent connections (Keep-Alive).
DisableKeepAlives: false,
}
// Creating a fully custom *http.Client with the transport and timeout settings.
customHTTPClient := &http.Client{
// Sets a global timeout for all requests.
Timeout: 5 * time.Second,
}
// Creating a client using both the custom transport and other configurations.
cClient := client.New(
// Applying the custom HTTP client.
client.WithCustomHTTPClient(customHTTPClient),
// Custom context for request cancellation and deadlines.
client.WithContext(context.Background()),
client.WithHeaders(map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
}),
// Applying the custom transport.
client.WithTransport(customTransport),
// Setting a timeout for requests.
client.WithTimeout(5*time.Second),
// Retry on specific status codes.
client.WithRetry(
client.RetryConfig{
MaxRetries: 2,
Delay: 1 * time.Second,
UseBackoff: true,
Statuses: []int{500},
FailoverURLs: []string{"http://hosterror", "https://httpbin.org/post"},
EnableLog: true,
}),
)
// call client to POST
resp, err := cClient.Post("https://httpbin_error.org/post", map[string]string{"message": "Quick in action"})
if err != nil {
log.Fatal(err)
}
// show resp
fmt.Println("POST response:\n", string(resp.Body))
}
{"time":"2025-03-14T15:31:11.027180616-03:00","level":"WARN","msg":"Retrying request","url":"https://httpbin_error.org/post","method":"POST","attempt":1,"failover":1}
{"time":"2025-03-14T15:31:12.028294877-03:00","level":"WARN","msg":"Retrying request","url":"http://hosterror","method":"POST","attempt":2,"failover":2}
POST response:
{
"args": {},
"data": "{\"message\":\"Quick in action\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Length": "29",
"Content-Type": "application/json",
"Host": "httpbin_error.org",
"User-Agent": "Go-http-client/1.1",
"X-Amzn-Trace-Id": "Root=1-67d475f2-713b9e4c2fff65d413fcd097"
},
"json": {
"message": "Quick in action"
},
"origin": "179.216.110.129",
"url": "https://httpbin_error.org/post"
}
Explore how to set up an HTTP client that not only adheres to security best practices with TLS configurations but also ensures your application remains operational through network issues. This example includes detailed setups for handling HTTP client retries and switching to failover URLs when typical requests fail. Ideal for systems requiring high reliability and fault tolerance.
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Creating a custom HTTP transport with advanced settings.
customTransport := &http.Transport{
// Uses system proxy settings if available.
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
// Allows insecure TLS connections (not recommended for production).
InsecureSkipVerify: true,
// Enforces a minimum TLS version for security.
MinVersion: tls.VersionTLS12,
},
// Maximum number of idle connections across all hosts.
MaxIdleConns: 50,
// Maximum simultaneous connections per host.
MaxConnsPerHost: 30,
// Maximum number of idle connections per host.
MaxIdleConnsPerHost: 10,
// Enables persistent connections (Keep-Alive).
DisableKeepAlives: false,
}
// Creating a fully custom *http.Client with the transport and timeout settings.
customHTTPClient := &http.Client{
// Sets a global timeout for all requests.
Timeout: 5 * time.Second,
// Uses the custom transport.
Transport: customTransport,
}
// Creating a client using both the custom transport and other configurations.
cClient := client.New(
// Applying the custom HTTP client.
client.WithCustomHTTPClient(customHTTPClient),
// Custom context for request cancellation and deadlines.
client.WithContext(context.Background()),
client.WithHeaders(map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
}),
client.WithTimeout(5*time.Second), // Setting a timeout for requests.
// Retry on specific status codes.
client.WithRetry(
client.RetryConfig{
MaxRetries: 2,
Delay: 1 * time.Second,
UseBackoff: true,
Statuses: []int{500},
FailoverURLs: []string{"http://hosterror", "https://httpbin.org/post"},
EnableLog: true,
}),
)
resp, err := cClient.Post("https://httpbin_error.org/post", map[string]string{"name": "jeffotoni"})
if err != nil {
log.Fatalf("POST request failed: %v", err)
}
// show resp
fmt.Println("POST response:", string(resp.Body))
}
{"time":"2025-03-14T15:37:43.481220287-03:00","level":"WARN","msg":"Retrying request","url":"https://httpbin_error.org/post","method":"POST","attempt":1,"failover":1}
{"time":"2025-03-14T15:37:44.482388761-03:00","level":"WARN","msg":"Retrying request","url":"http://hosterror","method":"POST","attempt":2,"failover":2}
POST response: {
"args": {},
"data": "{\"name\":\"jeffotoni\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Length": "20",
"Content-Type": "application/json",
"Host": "httpbin_error.org",
"User-Agent": "Go-http-client/1.1",
"X-Amzn-Trace-Id": "Root=1-67d4777b-50d494284d3d242224dc62c0"
},
"json": {
"name": "jeffotoni"
},
"origin": "179.216.110.129",
"url": "https://httpbin_error.org/post"
}
Discover how to build an HTTP client capable of dealing with network instabilities and server failures. This setup includes detailed retry configurations and introduces failover URLs to ensure that your application can maintain communication under adverse conditions. The example demonstrates using exponential backoff for retries and provides multiple endpoints to guarantee the availability of services.
package main
import (
"fmt"
"log"
"time"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create a new HTTP client with specific configurations.
cClient := client.New(
// Set a timeout for all requests made by this client to 10 seconds.
// This helps prevent the client from hanging indefinitely on requests.
client.WithTimeout(10*time.Second),
// Set default headers for all requests made by this client.
// Here, 'Content-Type' is set to 'application/json'
// which is typical for API calls.
client.WithHeaders(map[string]string{
"Content-Type": "application/json",
}),
// Enable automatic retry mechanism with specific configurations.
// This is useful for handling intermittent errors and ensuring robustness.
client.WithRetry(
client.RetryConfig{
// Retry failed requests up to two times.
MaxRetries: 2,
// Wait for 1 second before retrying.
Delay: 1 * time.Second,
// Use exponential backoff strategy for retries.
UseBackoff: true,
// HTTP status codes that trigger a retry.
Statuses: []int{500, 502, 503},
// Alternate URLs to try if the main request fails.
FailoverURLs: []string{
"http://hosterror",
"https://httpbin.org/post",
},
// Enable logging for retry operations.
EnableLog: true,
}),
)
// Perform a POST request using the configured HTTP client.
// Includes a JSON payload with a "name" key.
resp, err := cClient.Post("https://httpbin_error.org/post", map[string]string{
"name": "jeffotoni in action with Quick!!!",
})
// Check if there was an error with the POST request.
if err != nil {
// If an error occurs, log the error and terminate the program.
log.Fatalf("POST request failed: %v", err)
}
// Print the response from the server to the console.
fmt.Println("POST Form Response:", string(resp.Body))
}
{"time":"2025-03-14T15:40:30.617507958-03:00","level":"WARN","msg":"Retrying request","url":"https://httpbin_error.org/post","method":"POST","attempt":1,"failover":1}
{"time":"2025-03-14T15:40:31.618144855-03:00","level":"WARN","msg":"Retrying request","url":"http://hosterror","method":"POST","attempt":2,"failover":2}
POST Form Response: {
"args": {},
"data": "{\"name\":\"jeffotoni in action with Quick!!!\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "44",
"Content-Type": "application/json",
"Host": "httpbin_error.org",
"User-Agent": "Go-http-client/1.1",
"X-Amzn-Trace-Id": "Root=1-67d47822-5c80648f5a30c75c6a500470"
},
"json": {
"name": "jeffotoni in action with Quick!!!"
},
"origin": "179.216.110.129",
"url": "https://httpbin_error.org/post"
}
Explore the configuration of an HTTP client designed for high reliability and security in network communications. This example includes sophisticated transport settings, featuring TLS configurations for enhanced security, and a robust retry mechanism to handle request failures gracefully. These settings are essential for applications requiring reliable data exchange with external APIs, especially in environments where network stability might be a concern.
package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
"github.com/jeffotoni/quick/http/client"
)
func main() {
// Create an HTTP client with custom configurations using the Quick framework.
cClient := client.New(
// Set a global timeout for all requests made by this client to 10 seconds.
// This helps prevent the client from hanging indefinitely on requests.
client.WithTimeout(10*time.Second),
// Set default headers for all requests made by this client.
// Here, we specify that we expect to send and receive JSON data.
client.WithHeaders(map[string]string{"Content-Type": "application/json"}),
// Configure the underlying transport for the HTTP client.
client.WithTransportConfig(&http.Transport{
// Use the system environment settings for proxy configuration.
Proxy: http.ProxyFromEnvironment,
// Configure TLS settings to skip verification of the server's
// certificate chain and hostname.
// Warning: Setting InsecureSkipVerify to true is not recommended for
// production as it is insecure.
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
// Enable HTTP/2 for supported servers.
ForceAttemptHTTP2: true,
// Set the maximum number of idle connections in the connection pool for all hosts.
MaxIdleConns: 20,
// Set the maximum number of idle connections in the connection pool per host.
MaxIdleConnsPerHost: 10,
// Set the maximum number of simultaneous connections per host.
MaxConnsPerHost: 20,
// Keep connections alive between requests. This can help improve performance.
DisableKeepAlives: false,
}),
)
// Perform a POST request with a JSON payload.
// The payload includes a single field "name" with a value.
resp, err := cClient.Post("https://httpbin.org/post", map[string]string{"name": "jeffotoni"})
if err != nil {
// Log the error and stop the program if the POST request fails.
log.Fatalf("POST request failed: %v", err)
}
// Output the response from the POST request.
fmt.Println("POST Form Response:", string(resp.Body))
}
POST Form Response: {
"args": {},
"data": "{\"name\":\"jeffotoni\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "20",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Go-http-client/2.0",
"X-Amzn-Trace-Id": "Root=1-67d4786f-61ddc079287866e673f4f584"
},
"json": {
"name": "jeffotoni"
},
"origin": "179.216.110.129",
"url": "https://httpbin.org/post"
}
TLS (Transport Layer Security)
is a cryptographic protocol that provides secure communication over a network.
It is widely used to encrypt data transmitted between clients and servers, ensuring confidentiality, integrity, and authentication.
TLS is the successor to SSL (Secure Sockets Layer) and is used in HTTPS, email security, and many other applications.
πΉ Feature | π Description |
---|---|
π Encryption | Protects data from being intercepted during transmission. |
π Authentication | Ensures the server (and optionally the client) is legitimate. |
π Data Integrity | Prevents data from being modified or tampered with in transit. |
β‘ Performance | Modern TLS versions (1.2, 1.3) offer strong security with minimal overhead. |
This example demonstrates how to set up an HTTPS server using Quick with TLS encryption, ensuring secure communication between clients and the server.
package main
import (
"fmt"
"github.com/jeffotoni/quick"
)
func main() {
// Initialize Quick instance
q := quick.New()
// Print a message indicating that the server is starting on port 8443
fmt.Println("Run Server port:8443")
// Start the HTTPS server with TLS encryption
// - The server will listen on port 8443 (non-privileged port)
// - cert.pem: SSL/TLS certificate file
// - key.pem: Private key file for SSL/TLS encryption
err := q.ListenTLS(":8443", "cert.pem", "key.pem", false)
if err != nil {
// Log an error message if the server fails to start
fmt.Printf("Error when trying to connect with TLS: %v\n", err)
}
}
This example uses port 8443 so that it runs on any operating system without requiring extra permissions.
However, in production, you may want to use the standard HTTPS port 443.
- Port 443 (default for HTTPS) is a privileged port (below 1024).
- On Linux, running a service on port 443 requires superuser privileges.
To run on port 443 on Linux, use:
$ sudo go run main.go
The Rate Limiter is a middleware for the Quick framework that controls the number of requests allowed in a given time period. It helps prevent API abuse and improves system stability by preventing server overload.
Feature | Description |
---|---|
π― Request Rate Limiting | Configurable maximum number of requests per client within a time window. |
β³ Automatic Expiration | Resets the request counter automatically after the configured time. |
π Custom Client Identification | Uses a KeyGenerator function to define a unique client key (e.g., IP-based). |
Allows defining a custom response when the request limit is reached. | |
β‘ Efficient Performance | Implements sharding and optimizations to reduce concurrency issues. |
The example below shows how to apply the Rate Limiter as global middleware.
package main
import (
"time"
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/limiter"
)
func main() {
q := quick.New()
// Apply the rate limiter middleware
q.Use(limiter.New(limiter.Config{
// Maximum 10 requests allowed per IP
Max: 10,
// The limit resets after 5 seconds
Expiration: 5 * time.Second,
KeyGenerator: func(c *quick.Ctx) string {
// Uses the client's IP address as the key
return c.RemoteIP()
},
LimitReached: func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
// The client should wait 10 seconds before retrying
c.Set("Retry-After", "10")
return c.Status(quick.StatusTooManyRequests).JSON(map[string]string{
"error": "Too many requests",
"message": "You have exceeded the request limit.
Please wait 1 second and try again.",
"retry_after": "10s",
})
},
}))
// Define a simple GET route
q.Get("/", func(c *quick.Ctx) error {
return c.Status(200).JSON(map[string]string{"msg": "Quick in action β€οΈ!"})
})
// Start the server on port 8080
q.Listen(":8080")
}
$ curl -i -X GET http://localhost:8080/
If the same IP makes more than 10 requests in 5 seconds, the middleware returns:
{
"error": "Too many requests",
"message": "You have exceeded the request limit.
Please wait 1 second and try again.",
"retry_after": "10s"
}
The example below shows how to apply the Rate Limiter with route group.
package main
import (
"log"
"time"
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/limiter"
)
func main() {
// Create a new Quick instance
q := quick.New()
// Rate Limiter Middleware
limiterMiddleware := limiter.New(limiter.Config{
// Maximum 3 requests allowed per IP address within a 10-second window
Max: 3,
// The limit resets every 10 seconds
Expiration: 10 * time.Second,
// Use the client's IP address as the unique key to track rate limits
KeyGenerator: func(c *quick.Ctx) string {
return c.RemoteIP()
},
// If the rate limit is exceeded, send an error message and instructions
LimitReached: func(c *quick.Ctx) error {
// Set content type to JSON
c.Set("Content-Type", "application/json")
c.Set("Retry-After", "10")
// Response structure
response := map[string]string{
"error": "Too many requests",
"message": "You have exceeded the request limit.
Please wait 10 seconds and try again.",
"retry_after": "10s",
}
// Log to verify that the rate limit exceeded response is being sent
log.Println("Rate Limit Exceeded:", response)
// Return the response with HTTP status 429 (Too Many Requests)
return c.Status(quick.StatusTooManyRequests).JSON(response)
},
})
// Create an API group with rate limit middleware
api := q.Group("/v1")
// Apply the rate limiter middleware to the /api group
api.Use(limiterMiddleware)
// Define route /api/users that responds with a list of users
api.Get("/users", func(c *quick.Ctx) error {
return c.JSON(map[string]string{"msg": "list of users"})
})
// Define route /api/posts that responds with a list of posts
api.Get("/posts", func(c *quick.Ctx) error {
return c.JSON(map[string]string{"msg": "list of posts"})
})
// Define route without rate limit
q.Get("/", func(c *quick.Ctx) error {
return c.JSON(map[string]string{"msg": "Quick in action β€οΈ!"})
})
// Start the server on port 8080
q.Listen(":8080")
}
$ curl -i -X GET http://localhost:8080/users
If an IP makes more than 3 requests within 10 seconds, the response is blocked and returns a 429 Too Many Requests error:
{
"error": "Too many requests",
"message": "You have exceeded the request limit.
Please wait 10 seconds and try again.", "retry_after": "10s"
}
Benchmarking is a performance evaluation technique that measures response time, resource usage, and processing capacity. It helps developers identify bottlenecks and optimize their code. This approach is widely used in various areas, including software testing and hardware evaluations.
β Benefit | π Description |
---|---|
π Measure performance | Evaluates how a system responds under different workloads. |
π Compare technologies | Allows you to analyze different frameworks, libraries or implementations. |
π Identify bottlenecks | Helps detect critical points that need optimization. |
π Ensure scalability | Test system behavior with multiple simultaneous requests. |
π Simulate real-world scenarios | Reproduces heavy use situations, such as thousands of users accessing a service at the same time. |
To evaluate the performance of our API, we conducted a benchmark test using the Quick framework along with k6 for load testing.
The following Go API was used for benchmarking. It provides a POST endpoint at /v1/user, which:
- Accepts large JSON payloads.
- Parses the incoming JSON into a Go struct.
- Returns the parsed JSON as a response.
package main
import (
"github.com/jeffotoni/quick"
)
// Struct representing a user model
type My struct {
ID string `json:"id"`
Name string `json:"name"`
Year int `json:"year"`
Price float64 `json:"price"`
Big bool `json:"big"`
Car bool `json:"car"`
Tags []string `json:"tags"`
Metadata map[string]interface{} `json:"metadata"`
Options []Option `json:"options"`
Extra interface{} `json:"extra"`
Dynamic map[string]interface{} `json:"dynamic"`
}
type Option struct {
Key string `json:"key"`
Value string `json:"value"`
}
func main() {
// Initialize Quick framework with a 20MB body limit
q := quick.New(quick.Config{
MaxBodySize: 20 * 1024 * 1024,
})
// Define a POST route at /v1/user
q.Post("/v1/user", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
var users []My // Store incoming user data
// Parse the request body into the struct
err := c.Bind(&users)
if err != nil {
// If parsing fails, return a 400 Bad Request response
return c.Status(400).SendString(err.Error())
}
// Return the parsed JSON data as a response with 200 OK
return c.Status(200).JSON(users)
})
// Start the server and listen on port 8080
q.Listen("0.0.0.0:8080")
}
import http from 'k6/http';
import { check, sleep } from 'k6';
// Load the JSON from the environment variable
const payloadData = open('./data_1k_list.json');
// K6 configuration
export let options = {
stages: [
{ duration: '40s', target: 1000 }, // Ramp-up para 500 VUs
{ duration: '7s', target: 500 }, // MantΓ©m 500 VUs
{ duration: '5s', target: 0 }, // Ramp-down
],
};
export default function () {
let url = 'http://localhost:8080/v1/user';
// Always use the same list for sending
// let payload = JSON.stringify(payloadData);
let params = {
headers: { 'Content-Type': 'application/json' },
};
let res = http.post(url, payloadData, params);
// Check if the response is correct
check(res, {
'status is 200 or 201': (r) => r.status === 200 || r.status === 201,
'response contains JSON': (r) => r.headers['Content-Type'] === 'application/json',
});
}
- 1οΈβ£ Start the Quick API - Run the Quick server:
$ go run main.go
- 2οΈβ£ Execute the Load Test
$ k6 run benchmark.js
The compress middleware in Quick enables automatic GZIP compression for HTTP responses, reducing the size of data transferred over the network. This improves performance and bandwidth efficiency, especially for text-based content like JSON, HTML, and CSS.
- β Reduced response size β improves loading speed.
- β Bandwidth savings β ideal for mobile or limited connections.
- β Seamless integration β works automatically for compatible clients.
- β Better user experience β faster response times.
πΉ Ways to Use Quick provides three different ways to enable GZIP compression:
Quick provides three different ways to enable GZIP compression:
- Using quick.Handler (Default) β Follows Quickβs native syntax.
- Using quick.HandlerFunc β Alternative method for direct function-based handlers.
- Using net/http standard implementation β For applications using Goβs native HTTP package.
Here is a practical example of enabling the GZIP middleware in Quick using the default approach (quick.Handler)
package main
import (
"log"
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/compress"
)
func main() {
q := quick.New()
// Enable Gzip middleware
q.Use(compress.Gzip())
// Define a route that returns a compressed JSON response
q.Get("/v1/compress", func(c *quick.Ctx) error {
// Setting response headers
c.Set("Content-Type", "application/json")
// Enabling Gzip compression
c.Set("Accept-Encoding", "gzip")
// Defining the response structure
type response struct {
Msg string `json:"msg"`
Headers map[string][]string `json:"headers"`
}
// Returning a JSON response with headers
return c.Status(200).JSON(&response{
Msg: "Quick β€οΈ",
Headers: c.Headers,
})
})
// Start the HTTP server on port 8080
log.Fatal(q.Listen("0.0.0.0:8080"))
}
$ curl -X GET http://localhost:8080/v1/compress -H
"Accept-Encoding: gzip" --compressed -i
{
"msg":"Quick β€οΈ",
"headers":{
"Accept":[
"*/*"
],
"Accept-Encoding":[
"gzip"
],
"Cache-Control":[
"no-cache"
],
"Connection":[
"keep-alive"
],
"Postman-Token":[
"e0b65cfe-9516-4803-96df-d443d7e6a95a"
],
"User-Agent":[
"PostmanRuntime/7.43.2"
]
}
}
The maxbody middleware restricts the maximum request body size to prevent clients from sending excessively large payloads. This helps optimize memory usage, enhance security, and avoid unnecessary processing of oversized requests.
- β Prevents excessive memory usage and improves performance.
- β Mitigates DoS (Denial-of-Service) attacks by limiting large payloads.
- β Automatically returns 413 Payload Too Large when the limit is exceeded.
There are two primary ways to enforce request body size limits in Quick:
maxbody.New() β Enforces a global request body size limit across all middleware layers.
MaxBytesReader() β Adds an extra layer of validation inside a specific request handler
This example applies a global request body limit of 50KB for all incoming requests.
package main
import (
"log"
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/maxbody"
)
func main() {
q := quick.New()
// Middleware to enforce a 50KB request body limit globally
q.Use(maxbody.New(50 * 1024)) // 50KB
// Define a route that accepts a request body
q.Post("/v1/user/maxbody/any", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
log.Printf("Body received: %s", c.BodyString())
return c.Status(200).Send(c.Body())
})
log.Fatal(q.Listen("0.0.0.0:8080"))
}
Request within limit (Valid request)
$ curl -X POST http://0.0.0.0:8080/v1/user/maxbody/any \
-H "Content-Type: application/json" \
--data-binary @<(head -c 48000 </dev/zero | tr '\0' 'A')
Request exceeding limit (Should return 413)
$ curl -X POST http://0.0.0.0:8080/v1/user/maxbody/any \
-H "Content-Type: application/json" \
--data-binary @<(head -c 51000 </dev/zero | tr '\0' 'A')
This example adds extra protection by applying MaxBytesReader() inside the request handler, ensuring an enforced limit at the application layer.
package main
import (
"io"
"log"
"net/http"
"github.com/jeffotoni/quick"
)
const maxBodySize = 1024 // 1KB
func main() {
q := quick.New()
// Define a route with additional MaxBytesReader validation
q.Post("/v1/user/maxbody/max", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
// Limit request body size to 1KB
c.Request.Body = quick.MaxBytesReader(c.Response, c.Request.Body, maxBodySize)
// Securely read the request body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
log.Printf("Error reading request body: %v", err)
return c.Status(http.StatusRequestEntityTooLarge).String("Request body too large")
}
return c.Status(http.StatusOK).Send(body)
})
log.Println("Server running at http://0.0.0.0:8080")
log.Fatal(q.Listen("0.0.0.0:8080"))
}
Request within limit (Valid request)
$ curl -X POST http://0.0.0.0:8080/v1/user/maxbody/max \
-H "Content-Type: application/json" \
--data-binary @<(head -c 800 </dev/zero | tr '\0' 'A')
$ Request exceeding limit (Should return 413)
curl -X POST http://0.0.0.0:8080/v1/user/maxbody/max \
-H "Content-Type: application/json" \
--data-binary @<(head -c 2048 </dev/zero | tr '\0' 'A')
Implementation | Description |
---|---|
maxbody.New() |
Enforces a global request body size limit before processing the request. |
MaxBytesReader() |
Adds extra validation inside the request handler, restricting only specific endpoints. |
The logger
middleware captures HTTP request details, helping with monitoring, debugging, and analytics.
- β Logs request method, path, response time, and status code.
- β Supports multiple formats: text, json, and slog (structured logging).
- β Helps track API usage and debugging.
- β Customizable log patterns and additional fields.
This example applies logging in text format with custom log fields.
package main
import (
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/logger"
)
func main() {
q := quick.New()
// Apply the logger middleware with custom configuration
q.Use(logger.New(logger.Config{
Format: "text", // Available formats: "text", "json", "slog"
Pattern: "[${level}] ${ip} ${method} - ${latency} user_id=${user_id} trace=${trace}\n",
Level: "DEBUG", // Logging level: "DEBUG", "INFO", "WARN", "ERROR"
CustomFields: map[string]string{ // Custom fields included in logs
"user_id": "12345",
"trace": "xyz",
},
}))
// Define a route that logs request details
q.Get("/v1/logger", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
// Return a JSON response
return c.Status(200).JSON(quick.M{
"msg": "Quick β€οΈ",
})
})
// Start the server
q.Listen("0.0.0.0:8080")
}
Text Logging
$ curl -i -XGET http://localhost:8080/v1/logger
This example uses structured logging (slog) for better log parsing.
package main
import (
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/logger"
)
func main() {
q := quick.New()
// Apply logger middleware with structured logging (slog)
q.Use(logger.New(logger.Config{
Format: "slog",
Level: "DEBUG",
Pattern: "[${level}] ${ip} ${method} ${path} - ${latency} " +
"user=${user_id} trace=${trace}\n",
CustomFields: map[string]string{
"user_id": "99999",
"trace": "abcdef",
},
}))
// Define a route with structured logging
q.Get("/v1/logger/slog", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Status(200).JSON(quick.M{
"msg": "Structured logging with slog",
})
})
// Start the server
q.Listen("0.0.0.0:8080")
}
Structured Logging (Slog)
$ curl -i -XGET http://localhost:8080/v1/logger/slog
Ideal for log aggregation systems, this example logs in JSON format.
package main
import (
"github.com/jeffotoni/quick"
"github.com/jeffotoni/quick/middleware/logger"
)
func main() {
q := quick.New()
// Apply logger with JSON format for structured logging
q.Use(logger.New(logger.Config{
Format: "json",
Level: "INFO",
}))
// Define a logging route
q.Get("/v1/logger/json", func(c *quick.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Status(200).JSON(quick.M{
"msg": "JSON logging example",
})
})
// Start the server
q.Listen("0.0.0.0:8080")
}
JSON Logging
$ curl -i -XGET http://localhost:8080/v1/logger/json
This directory contains practical examples of the Quick Framework, a fast and lightweight web framework developed in Go.
The examples are organized in separate folders, each containing a complete example of using the framework in a simple web application.
If you have some interesting example of using the Quick Framework, feel free to send a Pull Request(PR) with your contribution.
We already have several examples, and we can already test and play π. Of course, we are at the beginning, still has much to do. Feel free to do PR (at risk of winning a Go t-shirt β€οΈ and of course recognition as a professional Go π in the labor market).
The Quick Project aims to develop and provide quality software for the developer community. π» To continue improving our tools, we rely on the support of our sponsors in Patreon. π€
We thank all our supporters! π If you also believe in our work and want to contribute to the advancement of the development community, consider supporting Project Quick on our Patreon here
Together we can continue to build amazing tools! π
π€ Avatar | π₯ User | π° Donation |
---|---|---|
@jeffotoni | β x 10 | |
@Crow3442 | β x 5 | |
@Guilherme-De-Marchi | β x 5 | |
@jaquelineabreu | β x 1 |