Due to Node 6 LTS end of life, the minimum Node version is 8.
In the previous version the mappingPolicy
default value was all
which means, you can call
any services via API Gateway which accepted by whitelist. This setting is not too secure.
From this version, the default value is restrict
if at least one alias is defined in route options.
If there are not aliases & mappingPolicy
defined, the behaviour will be the old one.
We have removed the middleware
service setting because it was not straightforward. Therefore, we have created a new server
setting.
If server: true
(which is the default value), API Gateway will create a HTTP(s) server. If server: false
, it won't create a HTTP server, so you can use API Gateway as an Express middleware.
Before
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
middleware: true
}
}
After
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
server: false
}
}
sendResponse
signature is changed:this.sendResponse(req, res, data)
API Gateway has implemented file uploads. You can upload files as a multipart form data (thanks for busboy library) or as a raw request body. In both cases, the file is transferred to an action as a Stream. In multipart form data mode you can upload multiple files, as well.
Please note, you have to disable other body parsers in order to accept files.
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
path: "/upload",
routes: [
{
path: "",
// You should disable body parsers
bodyParsers: {
json: false,
urlencoded: false
},
aliases: {
// File upload from HTML multipart form
"POST /": "multipart:file.save",
// File upload from AJAX or cURL
"PUT /": "stream:file.save",
// File upload from HTML form and overwrite busboy config
"POST /multi": {
type: "multipart",
// Action level busboy config
busboyConfig: {
limits: {
files: 3
}
},
action: "file.save"
}
},
// Route level busboy config.
// More info: https://github.com/mscdex/busboy#busboy-methods
busboyConfig: {
limits: {
files: 1
}
// Can be defined limit event handlers
// `onPartsLimit`, `onFilesLimit` or `onFieldsLimit`
},
mappingPolicy: "restrict"
}
]
}
});
HTTP2 experimental server has been implemented into API Gateway. You can turn it on with http2: true
service setting.
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
port: 8443,
// HTTPS server with certificate
https: {
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem")
},
// Use HTTP2 server
http2: true
}
});
The this.addRoute(opts, toBottom = true)
new service method is added to add/replace routes. You can call it from your mixins to define new routes (e.g. swagger route, graphql route...etc).
The function detects that the route is defined early. In this case, it will replace the previous route configuration with the new one.
To remove a route, use the this.removeRoute("/admin")
method. It removes the route by path.
Thank to tiaod for ETag implementation. PR: #92
Example
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: false,
routes: [
{
path: "/",
// Route-level option.
etag: true
}
]
}
}
The etag
option value can be false
, true
, weak
, strong
, or a custom Function.
Custom ETag generator function
module.exports = {
mixins: [ApiGateway],
settings: {
// Service-level option
etag: (body) => generateHash(body)
}
}
Please note, it doesn't work with stream responses. In this case, you should generate the etag by yourself.
Example
module.exports = {
name: "export",
actions: {
// Download response as a file in the browser
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`,
"ETag": '<your etag here>'
};
return csvFileStream;
}
}
}
The auto-aliasing means you don't have to add all service aliases to the routes, the Gateway can generate it from service schema. If a new service is entered or leaved, Gateway regenerate aliases.
To configure which services are used in route use the whitelist.
Example
// api.service.js
module.exports = {
mixins: [ApiGateway],
settings: {
routes: [
{
path: "/api",
whitelist: [
"posts.*",
"test.*"
],
aliases: {
"GET /hi": "test.hello"
},
autoAliases: true
}
]
}
};
// posts.service.js
module.exports = {
name: "posts",
version: 2,
settings: {
// Base path
rest: "posts/"
},
actions: {
list: {
// Expose as "/v2/posts/"
rest: "GET /",
handler(ctx) {}
},
get: {
// Expose as "/v2/posts/:id"
rest: "GET /:id",
handler(ctx) {}
},
create: {
rest: "POST /",
handler(ctx) {}
},
update: {
rest: "PUT /:id",
handler(ctx) {}
},
remove: {
rest: "DELETE /:id",
handler(ctx) {}
}
}
};
The generated aliases
GET /api/hi => test.hello
GET /api/v2/posts => v2.posts.list
GET /api/v2/posts/:id => v2.posts.get
POST /api/v2/posts => v2.posts.create
PUT /api/v2/posts/:id => v2.posts.update
DELETE /api/v2/posts/:id => v2.posts.remove
If
rest: true
in service settings, API Gateway will use the service name (with version) as base path.
If
rest: true
in action definition, API Gateway will use action name in path.
- new
optimizeOrder: true
setting in order to optimize route paths (deeper first). Defaults:true
. - tilde (
~
) replace issue fixed. #98 - throw
503
-ServiceUnavailableError
when a service defined in aliases but not available. Ref: #27 - new
internalServiceSpecialChar
service setting to override special char for internal services (~
)
- allow multiple whitespaces between method & path in aliases.
- fix
req.url
, addreq.originalUrl
andreq.baseUrl
for better middleware support (e.g. support static serving in subpath). - update deps
- use
Promise
instarted
&stopped
handlers. - disable 4xx errors with
log4XXResponses
setting.
This authenticate
method is similar to authorize
. You have access to req
, res
and route
objects and you can authenticate the user from the request.
The returned data is saved to the ctx.meta.user
. To enable this logic set authentication: true
in route options.
Example
module.exports = {
name: "api",
mixins: [ApiGatewayService],
settings: {
routes: [
{
// Enable authentication
authentication: true
}
]
},
methods: {
authenticate(ctx, route, req, res) {
let accessToken = req.query["access_token"];
if (accessToken) {
if (accessToken === "12345") {
return Promise.resolve({ id: 1, username: "john.doe", name: "John Doe" });
} else {
return Promise.reject();
}
} else {
return Promise.resolve(null);
}
}
}
});
- update dependencies.
- added
.npmignore
- fix missing dependency.
- fix middleware array promise-chaining bug
- handle terminated requests in middlewares
- update webpack-vue example to be up-to-date.
In previous versions of Moleculer Web, you couldn't manipulate the data
in onAfterCall
. Now you can, but you must always return the new or original data
.
Modify only headers
broker.createService(ApiGatewayService, {
settings: {
routes: [{
onAfterCall(ctx, route, req, res, data) {
res.setHeader("X-Custom-Header", "123456");
// Must return the original `data`
return data;
}
}]
}
});
Modify (wrap) the original data
broker.createService(ApiGatewayService, {
settings: {
routes: [{
onAfterCall(ctx, route, req, res, data) {
// Wrap the original data to a new object
return {
other: "things",
data: data
};
}
}]
}
});
The onBeforeCall
and authorize
hooks are called before custom alias functions too.
And you have access to Context as req.$ctx
or res.$ctx
In early versions the *
match string is enabled to call all services & actions. The matcher changed, in new versions use the **
(double star) match string for the same function.
Since Moleculer v0.12, you can use ctx.meta
to send back response headers to the Moleculer Web.
The old method is deprecated but works.
Available meta fields:
ctx.meta.$statusCode
- setres.statusCode
.ctx.meta.$statusMessage
- setres.statusMessage
.ctx.meta.$responseType
- setContent-Type
in header.ctx.meta.$responseHeaders
- set all keys in header.ctx.meta.$location
- setLocation
key in header for redirects.
Old method
module.exports = {
name: "export",
actions: {
downloadCSV:
responseType: "text/csv",
responseHeaders: {
"Content-Disposition": "attachment; filename=\"data.csv\"",
},
handler() {
return "...";
}
}
}
}
New method
module.exports = {
name: "export",
actions: {
// Download a file in the browser
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`
};
return "...";
}
// Redirect the request
redirectSample(ctx) {
ctx.meta.$statusCode = 302;
ctx.meta.$location = "/test/hello";
}
}
}
Thanks for @hwuethrich, Moleculer Web supports arrays & nested objects in querystring.
GET /api/opt-test?a=1&a=2
a: ["1", "2"]
GET /api/opt-test?foo[bar]=a&foo[bar]=b&foo[baz]=c
foo: {
bar: ["a", "b"],
baz: "c"
}
There is support to use error-handler middlewares in the API Gateway. So if you pass an Error
to the next(err)
function, it will call error handler middlewares which have signature as (err, req, res, next)
.
broker.createService({
mixins: [ApiService],
settings: {
// Global middlewares. Applied to all routes.
use: [
cookieParser(),
helmet()
],
routes: [
{
path: "/",
// Route-level middlewares.
use: [
compression(),
passport.initialize(),
passport.session(),
function(err, req, res, next) {
this.logger.error("Error is occured in middlewares!");
this.sendError(req, res, err);
}
],
preValidate
has been removed.- fix multiple CORS origin handling. Thanks for @felipegcampos
- if
X-Correlation-Id
is in the request header, it is used asrequestID
inContext
. - types in errors have been changed (removed
ERR_
prefix) path-to-regexp
is updated to v2.x.x
- update dependencies.
- fix Bluebird cancellation error
UnhandledPromiseRejectionWarning: Error: cannot enable cancellation after promises are in use
#202 -update dependencies
- turnable pre-validation with
preValidate
setting. Default totrue
in order to backward compatibility.broker.createService({ mixins: [ApiService], settings: { // Disable pre-validation at action calls preValidate: false } })
- fix CORS
OPTIONS
handling. #30
The route
first argument is removed. The new signature of function is function(req, res) {}
. To access to route use the req.$route
property.
However you can use an array of Function
for aliases. With it you can call middlewares. In this case the third argument is next
. I.e.: function(req, res, next) {}
.
- better error handling. Always returns with JSON error response.
- The charset is
UTF-8
forapplication/json
responses. logRequestParams
setting to log the request parameters. Use log level value i.e."debug"
,"info"
ornull
to disable.logResponseData
setting to log the response data. Use log level value i.e."debug"
,"info"
ornull
to disable.req.$service
&res.$service
is pointed to the service instance.req.$route
&res.$route
is pointed to the route definition.req.$params
is pointed to the resolved parameters (from query string & post body)req.$alias
is pointed to the alias definition.req.$endpoint
is pointed to the resolved action endpoint. It containsaction
andnodeID
.
Support middlewares in global, routes & aliases.
broker.createService({
mixins: [ApiService],
settings: {
// Global middlewares. Applied to all routes.
use: [
cookieParser(),
helmet()
],
routes: [
{
path: "/",
// Route-level middlewares.
use: [
compression(),
passport.initialize(),
passport.session(),
serveStatic(path.join(__dirname, "public"))
],
aliases: {
"GET /secret": [
// Alias-level middlewares.
auth.isAuthenticated(),
auth.hasRole("admin"),
"top.secret" // Call the `top.secret` action
]
}
}
]
}
});
It supports custom response headers to define in action definition.
module.exports = {
name: "export",
actions: {
downloadCSV:
responseHeaders: {
"Content-Disposition": "attachment; filename=\"data.csv\"",
"Content-Type": "text/csv"
},
handler() {
return "...";
}
}
}
}
You can add route & global custom error handlers.
broker.createService({
mixins: [ApiService],
settings: {
routes: [{
path: "/api",
// Route error handler
onError(req, res, err) {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.writeHead(500);
res.end(JSON.stringify(err));
}
}],
// Global error handler
onError(req, res, err) {
res.setHeader("Content-Type", "text/plain");
res.writeHead(501);
res.end("Global error: " + err.message);
}
}
}
- Webpack - webpack-dev-middleware example
- Webpack & Vue - Webpack, VueJS, HMR example
- add
mappingPolicy
route option
- add CORS headers
- add Rate limiter
- compatibility with Moleculer >= v0.11.x
- update Moleculer to v0.10
module.exports = {
name: "test",
actions: {
dangerZone: {
publish: false,
handler(ctx) {
return "You cannot call this action via API Gateway!";
}
}
}
};
The route
has a callOptions
property which is passed to broker.call
. So you can set timeout
, retryCount
or fallbackResponse
options for routes.
broker.createService(ApiGatewayService, {
settings: {
routes: [{
callOptions: {
timeout: 1000, // 1 sec
retryCount: 0,
//fallbackResponse: { ... },
// or
//fallbackResponse(ctx, err) { ... }
}
}]
}
});
- in the REST shorthand, the
GET /
calls thelist
action instead offind
. The reason islist
action inmoleculer-db
is support pagination
- changed order of param handling
ctx.params = Object.assign({}, body, query, params)
. - moved
onBeforeCall
beforeauthorize
in request flow. So you can also reach unauthorized requests inonBeforeCall
handler. - the
sendResponse
method has new arguments:sendResponse(ctx, route, req, res, data, responseType)
There is available to use custom function in aliases. In this case you got req
& res
and you should return with the response. Use it for example file uploads. You can find example in the full example.
Usage
...
aliases: {
"add/:a/:b": "math.add",
"GET sub": "math.sub",
"POST upload"(route, req, res) {
//Do something and call res.end()
}
}
...
There is a new camelCaseNames
option in route setting. If it is true, the service will convert the received action name to camelCase name.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
camelCaseNames: true
}]
}
});
broker.createService({
name: "test",
actions: {
sayHi(ctx) {
return "Hi!"
}
}
});
// Start server
broker.start();
In the above example the sayHi
action can be called with http://localhost:3000/test/say-hi as well.
Available errors:
Class | Params | Description |
---|---|---|
UnAuthorizedError |
type , data |
Unauthorized HTTP error (401) |
ForbiddenError |
type , data |
Forbidden HTTP error (403) |
BadRequestError |
type , data |
Bad Request HTTP error (400) |
Type contants:
ERR_NO_TOKEN
ERR_INVALID_TOKEN
ERR_UNABLE_DECODE_PARAM
Usage
const { UnAuthorizedError, ERR_NO_TOKEN } = require("moleculer-web").Errors;
...
actions: {
update(ctx) {
if(!ctx.meta.user)
return Promise.reject(new UnAuthorizedError(ERR_NO_TOKEN));
}
}
...
It is possible to use RESTful aliases which routed to CRUD service actions.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
// RESTful aliases
aliases: {
"REST posts": "posts"
}
}]
}
});
// Start server
broker.start();
The "REST posts": "posts"
will be extracted to these aliases:
"GET posts": "posts.find",
"GET posts/:id": "posts.get",
"POST posts": "posts.create",
"PUT posts/:id": "posts.update",
"DELETE posts/:id": "posts.remove"
Example: examples/rest
It is possible to use named parameters in aliases. Named paramters are defined by prefixing a colon to the parameter name (:name
)
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
path: "/api",
aliases: {
"GET greeter/:name": "test.greeter",
"optinal-param/:name?": "test.echo",
"repeat-param/:args*": "test.echo",
"GET /": "test.hello"
}
}]
}
});
// Start server
broker.start();
Example: examples/full
The route of service has onBeforeCall
and onAfterCall
hooks. It can be asynchronous if return with Promise. In methods the this
is pointed to Service instance. So you can access the service methods & broker.
Usage
broker.createService(ApiGatewayService, {
settings: {
routes: [{
// Call before `broker.call`
onBeforeCall(ctx, route, req, res) {
// Save request headers to context meta
ctx.meta.userAgent = req.headers["user-agent"];
},
// Call after `broker.call` and before send back the response
onAfterCall(ctx, route, req, res, data) {
res.setHeader("X-Custom-Header", "123456");
}
}]
}
});
// Start server
broker.start();
Example: examples/full
You can use Moleculer-Web as a middleware for ExpressJS.
Usage
const svc = broker.createService(ApiGatewayService, {
settings: {
middleware: true
}
});
// Create Express application
const app = express();
// Use ApiGateway as middleware
app.use("/api", svc.express());
// Listening
app.listen(3000);
// Start server
broker.start();
Example: examples/express
For more information check the full or authorization examples or readme
First release.