Categorygithub.com/DandyCodes/zeal
modulepackage
0.9.6
Repository: https://github.com/dandycodes/zeal.git
Documentation: pkg.go.dev

# README

     Logo

Zeal

✨ A type-safe REST API framework for Go!

About

  • Define structs to validate URL parameters, request bodies and responses.

  • Uses the standard library http.HandlerFunc for maximum compatibility.

  • URL parameters and request bodies are automatically converted to their declared type.

  • Automatically generates fully typed OpenAPI 3 spec documentation using REST and serves it with SwaggerUI.

Server

var mux = zeal.NewZealMux(http.NewServeMux(), "Example API")

func main() {
    addRoutes(mux)

    specOptions := zeal.SpecOptions{
        ZealMux:       mux,
        Version:       "v0.1.0",
        Description:   "Example API description.",
        StripPkgPaths: []string{"main", "models", "github.com/DandyCodes/zeal"},
    }
    openAPISpec, err := zeal.NewOpenAPISpec(specOptions)
    if err != nil {
        log.Fatalf("Failed to create OpenAPI spec: %v", err)
    }

    port := 3975
    swaggerPattern := "/swagger-ui/"
    fmt.Printf("Visit http://localhost:%v%v to see API definitions\n", port, swaggerPattern)
    zeal.ServeSwaggerUI(mux, openAPISpec, "GET "+swaggerPattern)

    fmt.Printf("Listening on port %v...\n", port)
    http.ListenAndServe(fmt.Sprintf(":%v", port), mux)
}

Routes

Create your route by calling zeal.NewRoute, passing it a zeal.ZealMux:

var route = zeal.NewRoute[zeal.Route](mux)

Passing the basic zeal.Route as a type parameter to zeal.NewRoute means this route has no:

  • Response type
  • URL parameters
  • Request body

Now, define your handler function using the newly created route:

route.HandleFunc("POST /hello", func(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Hello, world!")
})

Routes handled by Zeal are automatically documented in the OpenAPI spec.

Responses

Create a route definition struct and embed zeal.Route and zeal.HasResponse.

This route will respond with an integer, so int is passed to zeal.HasResponse as a type parameter:

type GetAnswer struct {
    zeal.Route
    zeal.HasResponse[int]
}

Create your route, passing your route definition as a type parameter, and define your handler function:

var getAnswer = zeal.NewRoute[GetAnswer](mux)
getAnswer.HandleFunc("GET /answer", func(w http.ResponseWriter, r *http.Request) {
    getAnswer.Response(42)
})

The Response method will only accept data of the declared response type.


Type parameters passed to zeal.HasResponse can be more complex.

Here is some example data:

var foodMenu = models.Menu{
    ID: 1,
    Items: []models.Item{
        {Name: "Steak", Price: 13.95},
        {Name: "Potatoes", Price: 3.95},
    },
}

var drinksMenu = models.Menu{
    ID: 2,
    Items: []models.Item{
        {Name: "Juice", Price: 1.25},
        {Name: "Soda", Price: 1.75},
    },
}

This route responds with a slice of menus:

var menus = []models.Menu{foodMenu, drinksMenu}

type GetMenus struct {
    zeal.Route
    zeal.HasResponse[[]models.Menu]
}
var getMenus = zeal.NewRoute[GetMenus](mux)
getMenus.HandleFunc("GET /menus", func(w http.ResponseWriter, r *http.Request) {
    getMenus.Response(menus)
})

URL Parameters

Create a route definition struct and embed zeal.Route and zeal.HasParams.

You can pass zeal.HasParams an anonymous in-line struct definition as a type parameter.

Create your route, passing your route definition as a type parameter, and define your handler function:

type DeleteMenu struct {
    zeal.Route
    zeal.HasParams[struct {
        ID    int
        Quiet bool
    }]
}
var deleteMenu = zeal.NewRoute[DeleteMenu](mux)
deleteMenu.HandleFunc("DELETE /menus/{ID}", func(w http.ResponseWriter, r *http.Request) {
    if !deleteMenu.Params().Quiet {
        fmt.Println("Deleting menu")
    }

    for i := 0; i < len(menus); i++ {
        if menus[i].ID == deleteMenu.Params().ID {
            menus = append(menus[:i], menus[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }

    w.WriteHeader(http.StatusNotFound)
})

Params found in the URL pattern (for example, 'ID' in '/menus/{ID}') will be defined as path params whilst others will be query params.

Params are converted to their declared type. If this fails, http.StatusUnprocessableEntity 422 is sent immediately.

Struct fields must be capitalized to be accessed in the route - for example, 'Quiet'.

Request Bodies

Create a route definition struct and embed zeal.Route and zeal.HasBody.

Pass the body type to zeal.HasBody as a type parameter.

Create your route, passing your route definition as a type parameter, and define your handler function:

type PutItem struct {
    zeal.Route
    zeal.HasBody[models.Item]
}
var putItem = zeal.NewRoute[PutItem](mux)
putItem.HandleFunc("PUT /items", func(w http.ResponseWriter, r *http.Request) {
    item := putItem.Body()
    if item.Price < 0 {
        http.Error(w, "Price cannot be negative", http.StatusBadRequest)
        return
    }

    for i := range menus {
        for j := range menus[i].Items {
            if menus[i].Items[j].Name == item.Name {
                menus[i].Items[j].Price = item.Price
                return
            }
        }
    }

    menus[0].Items = append(menus[0].Items, item)
    w.WriteHeader(http.StatusCreated)
})

The body is converted to its declared type. If this fails, http.StatusUnprocessableEntity 422 is sent immediately.

Struct fields must be capitalized to be accessed in the route - for example, 'Price'.

Error Handling

Use the HandleFuncErr method to create a handler function which returns an error.

Route handler functions can be defined in an outer scope:

var mux = zeal.NewServeMux(http.NewServeMux(), "Example API")

type PostItem struct {
    zeal.Route
    zeal.HasParams[struct{ MenuID int }]
    zeal.HasBody[models.Item]
    zeal.HasResponse[models.Item]
}

var postItem = zeal.NewRoute[PostItem](mux)

func addOuterScopeRoute() {
    postItem.HandleFuncErr("POST /items/{MenuID}", HandlePostItem)
}

func HandlePostItem(w http.ResponseWriter, r *http.Request) error {
    item := postItem.Body()
    if item.Price < 0 {
        return zeal.Error(w, "Price cannot be negative", http.StatusBadRequest)
    }

    for i := range menus {
        if menus[i].ID == postItem.Params().MenuID {
            menus[i].Items = append(menus[i].Items, item)
            return postItem.Response(item, http.StatusCreated)
        }
    }

    return zeal.WriteHeader(w, http.StatusNotFound)
}

The zeal.Error function returns a nil error after calling http.Error with a given error message and HTTP status code.

The Response method can be passed an optional HTTP status code (200 OK is sent by default). It returns a nil error if successful. Otherwise, it returns the JSON serialization error after calling http.Error with http.StatusInternalServerError.

The zeal.WriteHeader function returns a nil error after calling http.ResponseWriter.WriteHeader with a given HTTP status code.

Nested Handlers

Use zeal.ZealMux.Handle to preserve route documentation of sub handlers, using zeal.StripPrefix if necessary:

topMux := zeal.NewZealMux(http.NewServeMux(), "Example API")
subMux := zeal.NewZealMux(http.NewServeMux())
addRoutes(subMux)
topMux.Handle("/sub_api/", zeal.StripPrefix("/sub_api", subMux))

And use the top zeal.ZealMux to create the OpenAPI spec and listen and serve:

specOptions := zeal.SpecOptions{
    ZealMux:       topMux,
    Version:       "v0.1.0",
    Description:   "Example API description.",
    StripPkgPaths: []string{"main", "models", "github.com/DandyCodes/zeal"},
}
openAPISpec, err := zeal.NewOpenAPISpec(specOptions)
if err != nil {
    log.Fatalf("Failed to create OpenAPI spec: %v", err)
}

port := 3975
swaggerPattern := "/swagger-ui/"
fmt.Printf("Visit http://localhost:%v%v to see API definitions\n", port, swaggerPattern)
zeal.ServeSwaggerUI(topMux, openAPISpec, "GET "+swaggerPattern)

fmt.Printf("Listening on port %v...\n", port)
http.ListenAndServe(fmt.Sprintf(":%v", port), topMux)

Credits

Helmet icons created by Freepik - Flaticon

# Packages

No description provided by the author

# Functions

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Type aliases

No description provided by the author