A simple, easy to use, decorated, scoped, object-oriented web server, with async linear middlewares and no more callbacks in middlewares.
It is a web framework for typescript and nodejs.
It is also an example:
- To show that callbacks are not needed with promise/async/await.
- To use middlewares in a linear way instead of stacked way which is insecure.
For the stacked middleware model will carry response back to the top most so called middleware pushed, where every middleware can access to the body returned.
- To pass some vairiables through middlewares and to the final handler.
Aex is built and powerized by the following parts.
- Core functions
- Decorators
- Usage with no decorator
- Websocket support
- Middlewares
- Scope
- Express Middleware Integration
- Get the web server
- Keep in mind to separate web logic from business logic, and only develope for web logic.
- Focus soly on web flow.
- Simplify the way to make good web projects.
- Consider web interactions as phrased straight lines, which we call it Web Straight Line.
- No MVC, soly focusing on architecture which is the web logic.
npm install @aex/core # or npm i @aex/core
or if you use yarn
yarn add @aex/core
import { Aex, http } from "@aex/core";
class HelloAex {
private message = "aex";
@http("*", "*")
public all(_req: any, res: any, _scope: any) {
res.end("Hello " + this.message + "!");
}
}
// create Aex instance
const aex = new Aex();
// push your controller into aex
aex.push(HelloAex);
aex.prepare();
aex.start(8080).then();
// or
await aex.start(8080);
npm i @aex/core
or
yarn add @aex/core
prepare
is used here to init middlewares and controllers if controllers are pushed into the aex
instance. It takes no parameter and return the aex
instance. so you can invoke the start
function of aex.
await aex.prepare().start();
// or
aex
.prepare()
.start()
.then(() => {
// further processing
});
start
function is used to bootstrap the server with cerntain port. It takes three parameters:
port
the port taken by the web server, default to 3000ip
the ip address where the port bind to, default to localhostprepare
prepare middlewares or not, used when middlewares are not previously prepared
push a controller class to aex, it takes on parameter and other arguments:
- aClass: a class prototype.
- args: takes the rest arguments for the class constructor
aex.push(HelloAex);
//or
aex.push(HelloAex, parameter1, parameter2, ..., parameterN);
// will be invoked as `new HelloAlex(parameter1, parameter2, ..., parameterN)`
add middlewares to aex, see detailed explanation in middlewares
Aex is simplified by decorators, so you should be familiar with decorators to full utilize aex.
Decorators will be enriched over time. Currently aex provides 6 most important decorators. They are
@inject
, @http
, @body
, @query
, @filter
, @error
.
-
@inject
is the generate purpose decorator for client users to customize their handling. Users can inject any middleware with@inject
; -
@http
defines your http handler with a member function, it is the most important and fundamental decorator foraex
as a http web server. -
@body
defines your way to parse your body. -
@query
extract http query intoreq.query
andscope.query
; -
@filter
fiters and validates data from http requests, takesbody
,params
andquery
types only. -
@error
defines scoped errors
Aex provides the @http
decorator to ease the way http requests being handled by classes. It is very simple and intuitive.
The member methods are of IAsyncMiddleware
type as well.
@http
takes two parameter:
- http method name(s)
- url(s);
You can just pass url(s) if you use http GET
method only.
import { http } from "@aex/core";
class User {
@http("get", ["/profile", "/home"])
profile(req, res, scope) {}
@http(["get", "post"], "/user/login")
login(req, res, scope) {}
@http("post", "/user/logout")
logout(req, res, scope) {}
@http("/user/:id")
info(req, res, scope) {}
@http(["/user/followers", "/user/subscribes"])
followers(req, res, scope) {}
}
import { One } from "@aex/core";
const router = One.instance();
You need to create an instance of your class for request being processed.
const user = new User();
// do some initialization
import { Aex } from "@aex/core";
const aex = new Aex();
aex.use(router.toMiddleware());
aex.start();
Decorator @body provides a simple way to process data with body parser.
@body accept body parser package's function and its options, and they are optional.
@body("urlencoded", { extended: false })
and should succeed to @http decorator.
import { http } from "@aex/core";
class User {
@http("post", "/user/login")
@body("urlencoded", { extended: false })
login(req, res, scope) {}
@http("post", "/user/logout")
@body()
login(req, res, scope) {}
}
You may look up npm package body-parser
for detailed usage.
Decorator @query will parse query for you. After @query you will have req.query
to use.
@http("get", "/profile/:id")
@query()
public async id(req: any, res: any, _scope: any) {
// get /profile/111?page=20
req.query.page
// 20
}
Decorator @filter will filter body
, params
and query
data for you.
Reference node-form-validator for detailed usage.
class User {
private name = "Aex";
@http("post", "/user/login")
@body()
@filter({
body: {
username: {
type: "string",
required: true,
minLength: 4,
maxLength: 20
},
password: {
type: "string",
required: true,
minLength: 4,
maxLength: 64
}
},
fallbacks: {
body: async(error, req, res, scope) {
res.end("Body parser failed!");
}
}
})
public async login(req: any, res: any, _scope: any) {
// req.body.username
// req.body.password
}
@http("get", "/profile/:id")
@body()
@query()
@filter({
query: {
page: {
type: "numeric",
required: true
}
},
params: {
id: {
type: "numeric",
required: true
}
},
fallbacks: {
params: async function (this: any, _error: any, _req: any, res: any) {
this.name = "Alice";
res.end("Params failed!");
},
}
})
public async id(req: any, res: any, _scope: any) {
// req.params.id
// req.query.page
}
}
Decorator @error
will generate errors for you.
Reference errorable for detailed usage.
@error
take two parameters exactly what function Generator.generate
takes.
class User {
@http("post", "/error")
@error({
I: {
Love: {
You: {
code: 1,
messages: {
"en-US": "I Love U!",
"zh-CN": "我爱你!",
},
},
},
},
Me: {
alias: "I",
},
})
public road(_req: any, res: any, scope: any) {
const { ILoveYou } = scope.error;
// throw new ILoveYou('en-US');
// throw new ILoveYou('zh-CN');
res.end("User Error!");
}
}
Inject any middleware when necessary. But you should be careful with middlewares' order.
@inject
decrator takes two parameters:
- injector: the main injected middleware for data further processing or policy checking
- fallback: optional fallback when the injector fails and returned
false
class User {
private name = "Aex";
@http("post", "/user/login")
@body()
@inject(async (req, res, scope) => {
req.session = {
user: {
name: "ok"
}
};
})
@inject(async function(this:User, req, res, scope) {
this.name = "Peter";
req.session = {
user: {
name: "ok"
}
};
})
@inject(async function(this:User, req, res, scope) => {
this.name = "Peter";
if (...) {
return false
}
}, async function fallback(this:User, req, res, scope){
// some fallback processing
res.end("Fallback");
})
public async login(req: any, res: any, scope: any) {
// req.session.user.name
// ok
...
}
}
const aex = new Aex();
const router = new Router();
router.get("/", async (req, res, scope) => {
// request processing time started
console.log(scope.time.stated);
// processing time passed
console.log(scope.time.passed);
res.end("Hello Aex!");
});
aex.use(router.toMiddleware());
const port = 3000;
const host = "localhost";
const server = await aex.start(port, host);
// server === aex.server
- Create a
WebSocketServer
instance
const aex = new Aex();
const server = await aex.start();
const ws = new WebSocketServer(server);
- Get handler for one websocket connection
ws.on(WebSocketServer.ENTER, (handler) => {
// process/here
});
- Listen on user-customized events
ws.on(WebSocketServer.ENTER, (handler) => {
handler.on("event-name", (data) => {
// data.message = "Hello world!"
});
});
- Send message to browser / client
ws.on(WebSocketServer.ENTER, (handler) => {
handler.send("event-name", { key: "value" });
});
- New browser/client WebSocket object
const wsc: WebSocket = new WebSocket("ws://localhost:3000/path");
wsc.on("open", function open() {
wsc.send("");
});
- Listen on user-customized events
ws.on("new-message", () => {
// process/here
});
- Sending ws message in browser/client
const wsc: WebSocket = new WebSocket("ws://localhost:3000/path");
wsc.on("open", function open() {
wsc.send(
JSON.stringify({
event: "event-name",
data: {
message: "Hello world!",
},
})
);
});
- Use websocket middlewares
ws.use(async (req, ws, scope) => {
// return false
});
Global middlewares are effective all over the http request process.
They can be added by aex.use
function.
aex.use(async (req, res, scope) => {
// process 1
// return false
});
aex.use(async (req, res, scope) => {
// process 2
// return false
});
// ...
aex.use(async (req, res, scope) => {
// process N
// return false
});
Return
false
in middlewares will cancel the whole http request processing
It normally happens after ares.end
Handler specific middlewares are effective only to the specific handler.
They can be optionally added to the handler option via the optional attribute middlewares
.
the middlewares
attribute is an array of async functions of IAsyncMiddleware
.
so we can simply define handler specific middlewares as follows:
router.get(
"/",
async (req, res, scope) => {
res.end("Hello world!");
},
[
async (req, res, scope) => {
// process 1
// return false
},
async (req, res, scope) => {
// process 2
// return false
},
// ...,
async (req, res, scope) => {
// process N
// return false
},
]
);
Websocket middlewares are of the same to the above middlewares except that the parameters are of different.
type IWebSocketAsyncMiddleware = (
req: Request,
socket: WebSocket,
scope?: Scope
) => Promise<boolean | undefined | null | void>;
The Websocket Middlewares are defined as IWebSocketAsyncMiddleware
, they pass three parameters:
- the http request
- the websocket object
- the scope object
THe middlewares can stop websocket from further execution by return false
The node system http.Server
.
Accessable through aex.server
.
const aex = new Aex();
const server = await aex.start();
expect(server === aex.server).toBeTruthy();
server.close();
Aex provides scoped data for global and local usage.
A scope object is passed by middlewares and handlers right after req
, res
as the third parameter.
It is defined in IAsyncMiddleware
as the following:
async (req, res, scope) => {
// process N
// return false
};
the scope
variable has 8 native attributes: time
, outer
, inner
, query
, params
, body
, error
, debug
The time
attribute contains the started time and passed time of requests.
The outer
attribute is to store general or global data.
The inner
attribute is to store specific or local data.
The query
attribute is to store http query.
The body
attribute is to store http body.
The params
attribute is to store http params.
The error
attribute is to store scoped errors.
The debug
attribute is to provide handlers the debugging ability.
scope.time.started;
// 2019-12-12T09:01:49.543Z
scope.time.passed;
// 2019-12-12T09:01:49.543Z
The outer
and inner
variables are objects used to store data for different purposes.
You can simply assign them a new attribute with data;
scope.inner.a = 100;
scope.outer.a = 120;
debug
is provided for debugging purposes.
It is a simple import of the package debug
.
Its usage is of the same to the package debug
, go debug for detailed info.
Here is a simple example.
async (req, res, scope) => {
const { debug } = scope;
const logger = debug("aex:scope");
logger("this is a debugging info");
};
// scope.outer = {}; // Wrong operation!
// scope.inner = {}; // Wrong operation!
// scope.time = {}; // Wrong operation!
// scope.query = {}; // Wrong operation!
// scope.params = {}; // Wrong operation!
// scope.body = {}; // Wrong operation!
// scope.error = {}; // Wrong operation!
// scope.debug = {}; // Wrong operation!
// scope.time.started = {}; // Wrong operation!
// scope.time.passed = {}; // Wrong operation!
Aex provide a way for express middlewares to be translated into Aex middlewares.
You need just a simple call to toAsyncMiddleware
to generate Aex's async middleware.
const oldMiddleware = (_req: any, _res: any, next: any) => {
// ...
next();
};
const pOld = toAsyncMiddleware(oldMiddleware);
aex.use(pOld);
You should be cautious to use express middlewares. Full testing is appreciated.
npm install
npm test
Semver has been ruined node.js npm for a long time, aex will not follow it. Aex will warn every user to keep aex version fixed and take care whenever update to anew version. Aex follows a general versioning called Effective Versioning.
aex is anti-koa which is wrong and misleading just like semver.
aex is an anti BLM project and a protector of law and order.
MIT