SmartAPI allows to quickly implement solid REST APIs in Golang. The idea behind the project is to replace handler functions with ordinary looking functions. This allows service layer methods to be used as handlers. Designation of a dedicated API layer is still advisable in order to map errors to status codes, write cookies, headers, etc.
SmartAPI is based on github.com/go-chi/chi. This allows Chi middlewares to be used.
This example returns a greeting with a name based on a query param name
.
package main
import (
"fmt"
"log"
"net/http"
"github.com/mmbednarek/smartapi"
)
func MountAPI() http.Handler {
r := smartapi.NewRouter()
r.Get("/greeting", Greeting,
smartapi.QueryParam("name"),
)
return r.MustHandler()
}
func Greeting(name string) string {
return fmt.Sprintf("Hello %s!\n", name)
}
func main() {
log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}
$ curl '127.0.0.1:8080/greeting?name=Johnny'
Hello Johnny!
But it's a good practice to use your own handler functions for your api.
package main
import (
"encoding/base32"
"encoding/base64"
"log"
"net/http"
"github.com/mmbednarek/smartapi"
)
func MountAPI() http.Handler {
r := smartapi.NewRouter()
r.Route("/encode", func(r smartapi.Router) {
r.Post("/base64", base64.StdEncoding.EncodeToString)
r.Post("/base32", base32.StdEncoding.EncodeToString)
}, smartapi.ByteSliceBody())
r.Route("/decode", func(r smartapi.Router) {
r.Post("/base64", base64.StdEncoding.DecodeString)
r.Post("/base32", base32.StdEncoding.DecodeString)
}, smartapi.StringBody())
return r.MustHandler()
}
func main() {
log.Fatal(http.ListenAndServe(":8080", MountAPI()))
}
~ $ curl 127.0.0.1:8080/encode/base64 -d 'smartAPI'
c21hcnRBUEk=
~ $ curl 127.0.0.1:8080/encode/base32 -d 'smartAPI'
ONWWC4TUIFIES===
~ $ curl 127.0.0.1:8080/decode/base64 -d 'c21hcnRBUEk='
smartAPI
~ $ curl 127.0.0.1:8080/decode/base32 -d 'ONWWC4TUIFIES==='
smartAPI
You can use SmartAPI with service layer methods as shown here.
package main
import (
"log"
"net/http"
"github.com/mmbednarek/smartapi"
)
type Date struct {
Day int `json:"day"`
Month int `json:"month"`
Year int `json:"year"`
}
type User struct {
Login string `json:"login"`
Password string `json:"password,omitempty"`
Email string `json:"email"`
DateOfBirth Date `json:"date_of_birth"`
}
type Service interface {
RegisterUser(user *User) error
Auth(login, password string) (string, error)
GetUserData(session string) (*User, error)
UpdateUser(session string, user *User) error
}
func newHandler(service Service) http.Handler {
r := smartapi.NewRouter()
r.Post("/user", service.RegisterUser,
smartapi.JSONBody(User{}),
)
r.Post("/user/auth", service.Auth,
smartapi.PostQueryParam("login"),
smartapi.PostQueryParam("password"),
)
r.Get("/user", service.GetUserData,
smartapi.Header("X-Session-ID"),
)
r.Patch("/user", service.UpdateUser,
smartapi.Header("X-Session-ID"),
smartapi.JSONBody(User{}),
)
return r.MustHandler()
}
func main() {
svr := service.NewService() // Your service implementation
log.Fatal(http.ListenAndServe(":8080", newHandler(svr)))
}
Middlewares can be used just as in Chi. Use(...)
appends middlewares to be used.
With(...)
creates a copy of a router with chosen middlewares.
Routing works similarity to Chi routing. Parameters can be prepended to be used in all endpoints in that route.
r.Route("/v1/foo", func(r smartapi.Router) {
r.Route("/bar", func(r smartapi.Router) {
r.Get("/test", func(ctx context.Context, foo string, test string) {
...
},
smartapi.QueryParam("test"),
)
},
smartapi.Header("X-Foo"),
)
},
smartapi.Context(),
)
Legacy handlers are supported with no overhead. They are directly passed as ordinary handler functions. No additional variadic arguments are required for legacy handler to be used.
A handler function with error only return argument will return empty response body with 204 NO CONTENT status as default.
r.Post("/test", func() error {
return nil
})
Returned string will we written directly into a function body.
r.Get("/test", func() (string, error) {
return "Hello World", nil
})
Just as with the string, the slice will we written directly into a function body.
r.Get("/test", func() ([]byte, error) {
return []byte("Hello World"), nil
})
A struct, a pointer, an interface or a slice different than a byte slice with be encoded into a json format.
r.Get("/test", func() (interface{}, error) {
return struct{
Name string `json:"name"`
Age int `json:"age"`
}{"John", 34}, nil
})
To return an error with a status code you can use one of the error functions: smartapi.Error(status int, msg, reason string)
, smartapi.Errorf(status int, msg string, fmt ...interface{})
, smartapi.WrapError(status int, err error, reason string)
.
The API error contains an error message and an error reason. The message will be printed with a logger.
The reason will be returned in the response body. You can also return ordinary errors. They are treated as if their status code was 500.
r.Get("/order/{id}", func(orderID string) (*Order, error) {
order, err := db.GetOrder(orderID)
if err != nil {
if errors.Is(err, ErrNoSuchOrder) {
return nil, smartapi.WrapError(http.StatusNotFound, err, "no such order")
}
return nil, err
}
return order, nil
},
smartapi.URLParam("id"),
)
$ curl -i 127.0.0.1:8080/order/someorder
HTTP/1.1 404 Not Found
Date: Sun, 22 Mar 2020 14:17:34 GMT
Content-Length: 40
Content-Type: text/plain; charset=utf-8
{"status":404,"reason":"no such order"}
List of available endpoint attributes
Request can be passed into a structure's field by tags.
type headers struct {
Foo string `smartapi:"header=X-Foo"`
Bar string `smartapi:"header=X-Bar"`
}
r.Post("/user", func(h *headers) (string, error) {
return fmt.Sprintf("Foo: %s, Bar: %s\n", h.Foo, h.Bar), nil
},
smartapi.RequestStruct(headers{}),
)
Every argument has a tag value equivalent
Tag Value | Function Equivalent | Expected Type |
---|---|---|
header=name |
Header("name") |
string |
r_header=name |
RequiredHeader("name") |
string |
json_body |
JSONBody() |
... |
string_body |
StringBody() |
string |
byte_slice_body |
ByteSliceBody() |
[]byte |
body_reader |
BodyReader() |
io.Reader |
url_param=name |
URLParam("name") |
string |
context |
Context() |
context.Context |
query_param=name |
QueryParam("name") |
string |
r_query_param=name |
RequiredQueryParam("name") |
string |
post_query_param=name |
PostQueryParam("name") |
string |
r_post_query_param=name |
RequiredPostQueryParam("name") |
string |
cookie=name |
Cookie("name") |
string |
r_cookie=name |
RequiredCookie("name") |
string |
response_headers |
ResponseHeaders() |
smartapi.Headers |
response_cookies |
ResponseCookies() |
smartapi.Cookies |
response_writer |
ResponseWriter() |
http.ResponseWriter |
request |
Request() |
*http.Request |
request_struct |
RequestStruct() |
struct{...} |
as_int=header=name |
AsInt(Header("name") |
int |
as_byte_slice=header=name |
AsByteSlice(Header("name") |
[]byte |
JSON Body unmarshals the request's body into a given structure type. Expects a pointer to that structure as a function argument. If you want to use the object directly (not as a pointer) you can use JSONBodyDirect.
r.Post("/user", func(u *User) error {
return db.AddUser(u)
},
smartapi.JSONBody(User{}),
)
String body passes the request's body as a string.
r.Post("/user", func(body string) error {
fmt.Printf("Request body: %s\n", body)
return nil
},
smartapi.StringBody(),
)
Byte slice body passes the request's body as a byte slice.
r.Post("/user", func(body []byte) error {
fmt.Printf("Request body: %s\n", string(body))
return nil
},
smartapi.ByteSliceBody(),
)
Byte reader body passes the io.Reader interface to read request's body.
r.Post("/user", func(body io.Reader) error {
buff, err := ioutil.ReadAll()
if err != nil {
return err
}
return nil
},
smartapi.BodyReader(),
)
Classic http.ResponseWriter
can be used as well.
r.Post("/user", func(w http.ResponseWriter) error {
_, err := w.Write([]byte("RESPONSE"))
if err != nil {
return err
}
return nil
},
smartapi.ResponseWriter(),
)
Classic *http.Request
can be passed as an argument.
r.Post("/user", func(r *http.Request) error {
buff, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
fmt.Printf("Request body is: %s\n", string(buff))
return nil
},
smartapi.Request(),
)
Query param reads the value of the selected param and passes it as a string to function.
r.Get("/user", func(name string) (*User, error) {
return db.GetUser(name)
},
smartapi.QueryParam("name"),
)
Like QueryParam()
but returns 400 BAD REQUEST when empty.
r.Get("/user", func(name string) (*User, error) {
return db.GetUser(name)
},
smartapi.RequiredQueryParam("name"),
)
Reads a query param from requests body.
r.Get("/user", func(name string) (*User, error) {
return db.GetUser(name)
},
smartapi.PostQueryParam("name"),
)
Like PostQueryParam()
but returns 400 BAD REQUEST when empty.
r.Get("/user", func(name string) (*User, error) {
return db.GetUser(name)
},
smartapi.RequiredPostQueryParam("name"),
)
URL uses read chi's URL param and passes it into a function as a string.
r.Get("/user/{name}", func(name string) (*User, error) {
return db.GetUser(name)
},
smartapi.URLParam("name"),
)
Header reads the value of the selected request header and passes it as a string to function.
r.Get("/example", func(test string) (string, error) {
return fmt.Sprintf("The X-Test headers is %s", test), nil
},
smartapi.Header("X-Test"),
)
Like Header()
, but responds with 400 BAD REQUEST, if the header is not present.
r.Get("/example", func(test string) (string, error) {
return fmt.Sprintf("The X-Test headers is %s", test), nil
},
smartapi.RequiredHeader("X-Test"),
)
Reads a cookie from the request and passes the value into a function as a string.
r.Get("/example", func(c string) (string, error) {
return fmt.Sprintf("cookie: %s", c)
},
smartapi.Cookie("cookie"),
)
Like Cookie()
, but returns 400 BAD REQUEST when empty.
r.Get("/example", func(c string) (string, error) {
return fmt.Sprintf("cookie: %s", c)
},
smartapi.RequiredCookie("cookie"),
)
Context passes r.Context() into a function.
r.Get("/example", func(ctx context.Context) (string, error) {
return fmt.Sprintf("ctx: %s", ctx)
},
smartapi.Context(),
)
Response headers allows an endpoint to add response headers.
r.Get("/example", func(headers smartapi.Headers) error {
headers.Set("Api-Version", "1.2.3")
return nil
},
smartapi.ResponseHeaders(),
)
Response cookies allows an endpoint to easily add Set-Cookie header.
r.Get("/example", func(cookies smartapi.Cookies) error {
cookies.Add(&http.Cookie{Name: "Foo", Value: "Bar"})
return nil
},
smartapi.ResponseCookies(),
)
Request attributes can be automatically casted to desired type.
r.Get("/example", func(value int) error {
...
return nil
},
smartapi.AsInt(smartapi.Header("Value")),
)
r.Get("/example", func(value []byte) error {
...
return nil
},
smartapi.AsByteSlice(smartapi.Header("Value")),
)
Conversion to int
ResponseStatus allows the response status to be set for endpoints with empty response body. Default status is 204 NO CONTENT.
r.Get("/example", func() error {
return nil
},
smartapi.ResponseStatus(http.StatusCreated),
)