As stated in the module name, this project aims to create a layer to use Express.js like API on top of Go standard net/http.
API in ExpressGo aligns to specifications of Express.js 5.x API Reference.
ExpressGo leveraged ServeMux in net/http and would create a custom ServeMux with the following configurations.
- Host is not allowed in path matching.
- All path matching is precise.
- Path matching is case insensitive.
- Defining multiple lists of callbacks on the same route is allowed.
- Panics from callbacks would be recovered as an error and sent to error-handling callbacks.
To alter the behavior back to defaults of net/http:
config := expressgo.Config{}
config.AllowHost = true // to allow host
config.Coarse = true // to opt-out precise path matching
app := expressgo.CreateServer(config)
app.Set("case sensitive routing", true) // to use case sensitive path matching
To propagate panics in development mode:
- set
APP_ENV=development
in env - set
-mode=development
in flags (command arguments) - use
app.Set("APP_ENV", "development")
config := expressgo.Config{} // optional
app := expressgo.CreateServer(config)
// or, without a config
// app := expressgo.CreateServer()
// Get(string, func(*expressgo.Request, *expressgo.Response, *expressgo.Next))
app.Get("/hello", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send("Hello")
})
// With the style of middlewares
// Get(string, func1, func2, func3, ...)
// Listen(int)
app.Listen(8080) // 8080 is the port number
Path params should be in the form of :name
. A valid param name has the form of [A-Za-z_][A-Za-z0-9_]*
, starting with A-Z, a-z, or underscore (_), and concatenated with A-Z, a-z, 0-9, or underscore (_).
app.Get("/user/:id", func(req *expressgo.Request, res *expressgo.Response, *expressgo.Next) {
res.Send(req.Param["id"])
})
// Request: GET /user/101
// Respond: 101
To use separators, like hyphen (-) or dot (.):
app.Get("/test/:one-:two-:three/:four.:five", func(req *expressgo.Request, res *expressgo.Response, *expressgo.Next) {
lines := []string{}
for k, v := range req.Params {
lines = append(lines, fmt.Sprintf("%s: %s", k, v))
}
output := ""
for _, line := range lines {
output += line + "<br />"
}
res.Send(output)
})
// Request: GET /test/1-2-3/4.5
// Respond: one: 1<br />two: 2<br />three: 3<br />four: 4<br />five: 5<br />
Note:
- Paths should not contain
{}
. ExpressGo would treat it as a literal and pass it down tohttp.ServeMux
, and an error would occur.- Params should not have names ending with either
0H
or0D
. These two strings are used for separators, including hyphens and dots.
Query string could be read from req.Query["key"]
.
app.Get("/test/query", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send(req.Query["id"])
})
// Request: GET /test/query?id=101
// Respond: 101
Note:
- Query string would be parsed no matter with which http method.
- Only the first value of a key from the query string is parsed.
ExpressGo provides a package under github.com/Eandalf/expressgo/bodyparser for parsing the body of a request.
bodyparser.Json()
returns a parser as a middleware to parse received body stream with a specified type into req.Body
. It defaults to use expressgo.BodyJsonBase
, which is basically map[string]json.RawMessage
, as the received JSON type. Custom types could be supplied to the parser through bodyparser.Json(bodyparser.JsonConfig{Receiver: &Test{}})
where Test
is the name of the custom type. It is recommended to pass the pointer of the custom struct to Receiver
option since the underlying decoder is json.NewDecoder(...).Decode(...)
from encoding/json.
The parser leverages encoding/json. Hence, the custom struct should follow tag notations used in encoding/json.
For example,
type Test struct {
Test string `json:"test"`
}
To parse JSON with the default struct expressgo.BodyJsonBase
:
app.Post("/test/body/base", bodyparser.Json(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if j, ok := req.Body.(*expressgo.BodyJsonBase); ok {
if t, ok := (*j)["test"]; ok {
res.Send(string(t))
}
}
res.Send("body parsing failed")
})
// Request: POST /test/body/base/
// Body: '{"test":"test_string"}'
// Respond: test_string
To parse JSON with a custom struct:
type Test struct {
Test string `json:"test"`
}
app.Post("/test/body/type", bodyparser.Json(bodyparser.JsonConfig{Receiver: &Test{}}), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if t, ok := req.Body.(*Test); ok {
res.Send(t.Test)
}
res.Send("body parsing failed")
})
// Request: POST /test/body/type/
// Body: '{"test":"test_string"}'
// Respond: test_string
Note:
req.Body
is typed asinterface{}
.- Although it is common to set
bodyParser.json()
as a global middleware in Express.js, with static type constraints in Go, it is not idiomatic to do so. Since it is common to have callbacks for POST requests expecting different DTOs, it is more suitable to place the JSON parser on each route as shown in the examples above.bodyparser.Json()
could not be invoked twice on the same route (same method and same path), the parser would consume the body stream, which would lead to nothing left for the coming parser to process. If two JSON parsers are invoked, the second one would be a no-op instead of raising theio.EOF
error to the next error-handling callback.
Config options:
bodyparser.JsonConfig{
Receiver: any // pointer to the receiving struct
Type: any // expected type: string or []string
Inflate: bool
Limit: any // expected type: int64 or string
Verify: bodyparser.Verify // func(*expressgo.Request, *expressgo.Response, []byte, string) error
}
This middleware is provided under github.com/Eandalf/expressgo/bodyparser.
bodyparser.Raw()
returns a parser as a middleware to parse received body stream with a specified type into req.Body
. The type expected on req.Body
is []byte
.
app.Post("/test/body/raw", bodyparser.Raw(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if b, ok := req.Body.([]byte); ok {
res.Send(string(b))
}
})
Config options:
bodyparser.RawConfig{
Type: any // expected type: string or []string
Inflate: bool
Limit: any // expected type: int64 or string
Verify: bodyparser.Verify // func(*expressgo.Request, *expressgo.Response, []byte, string) error
}
This middleware is provided under github.com/Eandalf/expressgo/bodyparser.
bodyparser.Text()
returns a parser as a middleware to parse received body stream with a specified type into req.Body
. The type expected on req.Body
is string
.
app.Post("/test/body/text", bodyparser.Text(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if s, ok := req.Body.(string); ok {
res.Send(s)
}
})
Config options:
bodyparser.TextConfig{
Type: any // expected type: string or []string
Inflate: bool
Limit: any // expected type: int64 or string
Verify: bodyparser.Verify // func(*expressgo.Request, *expressgo.Response, []byte, string) error
DefaultCharset: string
}
This middleware is provided under github.com/Eandalf/expressgo/bodyparser.
bodyparser.Urlencoded()
returns a parser as a middleware to parse received body stream with a specified type into req.Body
. The type expected on req.Body
is expressgo.BodyFormUrlEncoded
, which is basically map[string]string
.
app.Post("/test/body/form/url", bodyparser.Urlencoded(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if f, ok := req.Body.(expressgo.BodyFormUrlEncoded); ok {
output := ""
for k, v := range f {
output += fmt.Sprintf("%s: %s<br />", k, v)
}
res.Send(output)
}
})
Config options:
bodyparser.TextConfig{
Type: any // expected type: string or []string
Inflate: bool
Limit: any // expected type: int64 or string
Verify: bodyparser.Verify // func(*expressgo.Request, *expressgo.Response, []byte, string) error
DefaultCharset: string
}
req.Get(string)
Get a request header specified by the field. The field is case-insensitive.
req.Header(string)
Alias of req.Get(string).
res.Send(string)
Send the response.
res.SendStatus(int)
Send the response with a status code.
res.Status(int)
Set the HTTP status code of the response. It is chainable.
res.End()
Stop further writes to the response.
res.Append(string, string)
Add a value to a response header, field: value. The field is case-insensitive.
res.Set(string, string)
Set a response header, field: value. The field is case-insensitive.
res.Get(string)
Get a response header specified by the field. The field is case-insensitive.
ExpressGo provides a package under github.com/Eandalf/expressgo/cors for setting CORS-related headers of a response.
// set a global CORS middleware
app.UseGlobal(cors.Use())
// set a per route CORS middleware
app.Get("/path", cors.Use(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {})
// set options by using cors.CorsConfig{}
app.UseGlobal(cors.Use(cors.CorsConfig{ /* options */ }))
Reference: cors configuration options.
Option Table:
cors | expressgo/cors |
---|---|
origin (String/Boolean/RegExp/Array) | Origin (string/bool/*regexp.Regexp/[]string) |
methods (String/Array) | Methods (string/[]string) |
allowedHeaders | AllowedHeaders |
exposedHeaders | ExposedHeaders |
credentials | Credentials |
maxAge | MaxAge |
preflightContinue | PreflightContinue |
optionsSuccessStatus | OptionsSuccessStatus |
At the current stage, it is still not possible to redifine function behaviors at runtime to mimic next()
or next('route')
usages in Express.js. Therefore, it is implemented this way to pass in a *Next
pointer to a callback, so a callback could either use next.Next = true
to activate the next callback or use next.Route = true
to activate another list of callbacks defined on the same route. After the aforementioned next.Next = true
or next.Route = true
statement, remember to add return
to exit the current callback if skipping any following logics is needed.
Note:
route
refers to the combination ofmethod
andpath
.next.Next
always takes precedence overnext.Route
if both are set.
To run the next callback:
// callback
func(*expressgo.Request, *expressgo.Response, next *expressgo.Next) {
next.Next = true
return
}
To run another list of callbacks defined on the same route:
// callback
func(*expressgo.Request, *expressgo.Response, next *expressgo.Next) {
next.Route = true
return
}
Note: The next list refers to the list defined after the current list, in the order being called using the same
app.[Method]
on the same path.
app.[Method]
To mount callbacks as middlewares to all paths with all http methods.
The order of invocation matters. The callbacks of app.[Method]
defined before app.UseGlobal
would be executed before the inserted middlewares using app.UseGlobal
.
app.UseGlobal(func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
req.Params["global"] = "global"
// next.Route is recommended to be set to `true`, otherwise, nothing after the middleware could be executed
next.Route = true
})
app.Get("/test/use/global", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send(req.Params["global"])
})
// Request: GET /test/use/global
// Respond: global
To mount callbacks as middlewares to the path with all http methods.
The order of invocation matters. The callbacks of app.[Method]
defined before app.Use
would be executed before the inserted middlewares using app.Use
.
app.Use("/test/use", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
req.Params["id"] = "101"
// next.Route is recommended to be set to `true`, otherwise, nothing after the middleware could be executed
next.Route = true
})
app.Get("/test/use", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send(req.Params["id"])
})
// Request: GET /test/use
// Respond: 101
For GET requests.
app.Get("/", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send("Hello from root")
})
// Request: GET /
// Respond: Hello from root
For POST requests.
app.Post("/test/body/base", bodyparser.Json(), func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
if j, ok := req.Body.(expressgo.BodyBase); ok {
if t, ok := j["test"]; ok {
if s, ok := t.(string); ok {
res.Send(s)
}
}
}
res.Send("body parsing failed")
})
// Request: POST /test/body/base/
// Body: '{"test":"test_string"}'
// Respond: test_string
- app.Head
app.Head(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
- app.Put
app.Put(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
- app.Patch
app.Patch(string, func(req *expressgo.Request, res *expressgo.Response, next *exp 5D32 ressgo.Next){})
- app.Delete
app.Delete(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
- app.Connect
app.Connect(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
- app.Options
app.Options(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
- app.Trace
app.Trace(string, func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next){})
If any error is intended to be handled by other callbacks, set next.Error = error
to pass the error to any error handler behind.
After an error handler is triggered, the error is seemed as consumed. If the error needs to be passed to another error handler, set next.Error = error
in the current error handler to pass the error down to the next error handler.
Error handlers are set with similar logics as app.Use
and app.UseGlobal
, so the order of invocation matters.
app.UseError
and app.UseGlobalError
are often used at the very end of all app.[Method]
calls.
To mount an error handler on a path with all http methods.
app.Get("/test/error", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
next.Err = errors.New("raised error in /test/error")
return // optional, to skip any logics behind
})
app.UseError("/test/error", func(err error, req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send(err.Error())
})
// Request: GET /test/error
// Respond: raised error in /test/error
To mount an error handler to all routes.
app.Get("/test/error/1", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
next.Err = errors.New("raised error in /test/error/1")
return // optional, to skip any logics behind
})
app.Get("/test/error/2", func(req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
next.Err = errors.New("raised error in /test/error/2")
return // optional, to skip any logics behind
})
app.UseGlobalError(func(err error, req *expressgo.Request, res *expressgo.Response, next *expressgo.Next) {
res.Send(err.Error())
})
// Request: GET /test/error/1
// Respond: raised error in /test/error/1
// Request: GET /test/error/2
// Respond: raised error in /test/error/2
- chainable methods with path already included
- mountable mini-app
This is currently still a hobby project for learning programming language Go. The module did not go through thorough testings and optimizations. Please use it at your own risk as stated in License.