Goal
A type-safe REST API framework for Go!
Inspired by FastAPI, types are used to define and validate path parameters, request bodies and responses.
Automatically generates OpenAPI 3 schema documentation and serves it using Swagger.
It builds upon chi (github.com/go-chi/chi) for routing and rest (github.com/a-h/rest) for generating the OpenAPI spec.
var randomComic = models.Comic{
Title: "Spider-Man",
Author: "Stan Lee",
}
var randomUser = models.User{
ID: 1,
Name: "Phillip J Fry",
FavouriteComic: randomComic,
}
func AddRoutes(router *goal.Router) {
// A goal route.
goal.Get("/", router, func(c goal.CtxR[any]) (int, any) {
return http.StatusOK, nil
})
// A goal route which returns a string to be sent in the http response.
goal.Get("/ping", router, func(c goal.CtxR[any]) (int, string) {
return http.StatusOK, "pong"
})
// An int.
goal.Get("/answer", router, func(c goal.CtxR[any]) (int, int) {
finished := true
if !finished {
/*
An unsuccessful status (not >= 200 and < 300)
means the placeholder 0 is not sent in the response.
*/
return http.StatusNotFound, 0
}
return http.StatusOK, 42
})
// Complex return types from goal routes are serialized to JSON.
goal.Get("/user/random", router, func(c goal.CtxR[any]) (int, models.User) {
return http.StatusOK, randomUser
})
/*
These routes have no path parameters, so no type argument
is passed to the Read Context (goal.CtxR[any]).
*/
goal.Get("/users", router, func(c goal.CtxR[any]) (int, []models.User) {
return http.StatusOK, []models.User{randomUser, randomUser, randomUser}
})
/*
Path parameter types are automatically validated (ID integer from url string).
The path parameters type is passed to the CtxR (goal.CtxR[IDPath]).
*/
type IDPath struct {
ID int
}
goal.Get("/user/{ID}", router, func(c goal.CtxR[IDPath]) (int, models.User) {
found := c.Path.ID == randomUser.ID
if !found {
// Unsuccessful status means no JSON is sent.
return http.StatusNotFound, models.User{}
}
return http.StatusOK, randomUser
})
// Order doesn't matter, only the name (must be capitalized for Go to export the field).
type NameAgePath struct {
Name string
Age int
}
goal.Get("/user/{Age}/{Name}", router, func(c goal.CtxR[NameAgePath]) (int, models.User) {
forbidden := c.Path.Age < 18
if forbidden {
return http.StatusForbidden, models.User{}
}
found := c.Path.Name == randomUser.Name
if !found {
return http.StatusNotFound, models.User{}
}
return http.StatusOK, randomUser
})
/*
No path parameters and no request body means no types are passed
to the Create/Update/Delete Context (goal.CtxCUD[any, any]).
*/
goal.Post("/user/ping", router, func(c goal.CtxCUD[any, any]) (int, models.User) {
return http.StatusOK, randomUser
})
// No request body type is passed to the CtxCUD, but the path type is (goal.CtxCUD[NamePath, any]).
type NamePath struct {
Name string
}
goal.Post("/user/{Name}", router, func(c goal.CtxCUD[NamePath, any]) (int, models.User) {
newUser := models.User{Name: c.Path.Name}
// Successful status code means the content is sent in the response
return http.StatusCreated, newUser
})
// No path type is passed, but the request body type is (goal.CtxCUD[any, models.User]).
goal.Put("/user", router, func(c goal.CtxCUD[any, models.User]) (int, models.User) {
updatedUser := c.Body
return http.StatusOK, updatedUser
})
// Both a path type and a request body type are passed (goal.CtxCUD[TitlePath, models.User]).
type TitlePath struct {
Title string
}
goal.Post("/user/{Title}", router, func(c goal.CtxCUD[TitlePath, models.User]) (int, models.User) {
newUser := c.Body
if c.Path.Title == randomComic.Title {
newUser.FavouriteComic = randomComic
}
return http.StatusOK, newUser
})
// Handler function and types defined in outer scope.
goal.Delete("/user/incomplete/{Age}", router, HandlePostIncompleteUser)
}
type AgePath struct {
Age int
}
// Ad-hoc request body type.
type IncompleteUser struct {
FavouriteComic models.Comic
}
func HandlePostIncompleteUser(c goal.CtxCUD[AgePath, IncompleteUser]) (int, models.User) {
forbidden := c.Path.Age < 18
if forbidden {
return http.StatusForbidden, models.User{}
}
found := randomUser.FavouriteComic == c.Body.FavouriteComic
if !found {
return http.StatusNotFound, models.User{}
}
userToDelete := randomUser
fmt.Println("Deleting", userToDelete)
// 204 No Content status means no JSON will be sent.
return http.StatusNoContent, models.User{}
}
/*
Path parameters, request bodies and return types
are documented in the automatically generated OpenAPI schema.
*/
/*
Standard chi routes can be defined using the goal router.
router.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
If you do, don't nest with this router within one with goal routes because you won't
be able to create an OpenAPI schema without additional type information.
*/