README
AEX
An Object-Oriented Web Framework needs no MVC
import { Aex, http } from "@aex/core";
class Helloworld {
public message: string;
constructor() {
this.message = "Hello world!";
}
@http("/")
public async all(req: any, res: any) {
res.end(this.message);
}
}
const aex = new Aex();
aex.push(Helloworld);
aex.prepare().start(8086);
Intro
A simple, easy to use, decorated, scoped, object-oriented web server, with async linear middlewares and no more callbacks in middlewares.
It can be used as a micro service server and a very large scalable enterprise web server with official or customized decorators plugged-in.
It is a web framework based on typescript and nodejs.
Philosophy
- 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.
What is Web Straight Line?
Web Straight Line is used to describe the phrases of the processes on the http/web request.
It can be breifly describe as the following diagram:
The Web Staight Line

Web Straight Line is a true web server thoery for the web framework (comparing to the MVC thoery which is more suitable for desktop applications), which solves only the problems caused by web (namely the HTTP protocol) itself.
It can be trimmed as a micro service web server or a full-fledged web server by docrating enough constraints using docrators, it is logic scalable by decorators/middlewares.
Content
- Install
- Quick Start
- Framework functions
- Decorators
- Usage with no decorators
- Middlewares
- Scope
- Helper
- Express Middleware Integration
- Get the web server
- Websocket support
- Upgrade from Expressjs
Shortcuts for decorators
HTTP method decorators (
@http,@get,@post)Data parsing decorators (
@formdata,@query,@body)Static file serving decorators (
@serve)Session management decorators (
@session)Data filtering and validation decorators (
@filter)Error definition decorators (
@error)Custome middleware decorators (
@inject)
Shortcuts for helpers
Install
if you use npm
npm install @aex/core # or npm i @aex/core
or if you use yarn
yarn add @aex/core
Quick Start
Use
@httpto enable a class with web abilityimport { Aex, http } from "@aex/core"; class HelloAex { private name = "Alice"; constructor(name: string) { this.name = name; } @http("*", "*") public all(_req: any, res: any, _scope: any) { res.end("Hello from " + this.name + "!"); } }Create an aex instance
const aex = new Aex();Push your class to aex with parameters if you
// push your handler into aex with constructor parameters in order aex.push(HelloAex, "Eric");Prepare aex enviroment
aex.prepare();Start aex web server
aex.start(8080).then(); // or await aex.start(8080);
A quick starter for javascript users.
Aex is written in typescript, but it can be very well used with javascript.
You can click here to get the starter for javascript developers.
it is located at: https://github.com/aex-ts-node/aex-babel-node-js-starter; You can refer it every time your want to create a new aex project.
Framework functions
The aex object has many functions for middlewares and classes.
They are:
- use To add a middleware
- push To push a class
- prepare To prepare the server
- start To start the server
use
Add middlewares to aex, see detailed explanations in middlewares. These middlewares will be global to all http requests.
push
push a controller class to aex, it takes at lease a class and its constructor's parameters followed by the class.
- 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)`
prepare
prepare is used here to init middlewares and request handlers written within use classes after these classes pushed to the aex instance. It takes no parameter and return the aex instance. so you can invoke the start function.
Aex introduces no MVC but the Web Straight Line to relect the flows how http requests are processed.
await aex.prepare().start();
// or
aex
.prepare()
.start()
.then(() => {
// further processing
});
start
start function is used to bootstrap the server with cerntain port. It takes three parameters:
portthe port taken by the web server, defaults to 3000ipthe ip address where the port binds to, defaults to localhostprepareprepare middlewares or not, used when middlewares are not previously prepared
Decorators
Aex is simplified by decorators, so you should be familiar with decorators to fully utilize aex.
Decorators will be enriched over time. Currently aex provides the following decorators:
- HTTP method decorators (
@http,@get,@post) - Data parsing decorators (
@formdata,@query,@body) - Static file serving decorators (
@serve) - Session management decorators (
@session) - Data filtering and validation decorators (
@filter) - Error definition decorators (
@error) - Custome middleware decorators (
@inject)
1. HTTP method decorators
This decorators are the most basic decorators, all decorators should follow them. They are
@http , @get , @post .
@http , @get , @post
@http is the generic http method decorator. @get , @post are the shortcuts for @http ;
The @http decorator defines your http handler with a member function.
The member methods are of IAsyncMiddleware type.
@http takes two parameter:
- http method name(s)
- url(s);
You can just pass url(s) if you use http GET method only or you can use @get .
Here is how your define your handlers.
import { http, get, post } from "@aex/core";
class User {
@http("get", ["/profile", "/home"])
profile() {}
@http(["get", "post"], "/user/login")
login() {}
@http("post", "/user/logout")
logout() {}
@http("/user/:id")
info() {}
@http(["/user/followers", "/user/subscribes"])
followers() {}
@get(["/user/get", "/user/gets"])
rawget() {}
@post("/user/post")
rawpost() {}
}
2. Data parsing decorators
These decorators will parse all data passed thought the HTTP protocol.
They are @formdata , @query , @body .
@formdatacan parsemulit-partformdata such as files intoscope.filesand other formdata intoscope.body. When parsed, you can retrieve yourmulti-partformdata fromscope.files,scope.body.@querycan parse url query intoscope.query.@bodycan parse some simple formdata intoscope.body.
@formdata
Decorator @formdata is a simplified version of node package busboy for aex , only the headers options will be auto replaced by aex . So you can parse valid options when necesary.
All uploaded files are in array format, and it parses body as well.
import { http, formdata } from "@aex/core";
class Formdata {
protected name = "formdata";
@http("post", "/file/upload")
@formdata()
public async upload(_req: any, res: any, scope: any) {
const { files, body } = scope;
// Access your files
const uploadedSingleFile = files["fieldname1"][0];
const uploadedFileArray = files["fieldname2"];
// Access your file info
uploadedSingleFile.temp; // temporary file saved
uploadedSingleFile.filename; // original filename
uploadedSingleFile.encoding; // file encoding
uploadedSingleFile.mimetype; // mimetype
// Access none file form data
const value = body["fieldname3"];
res.end("File Uploaded!");
}
}
@body
Decorator @body provides a simple way to process data with body parser. It a is a simplified version of node package body-parser.
It takes two parameters:
- types in ["urlencoded", "raw", "text", "json"]
- options the same as body-parser take.
then be parsed into scope.body , for compatibility req.body is still available.
Simply put:
@body("urlencoded", { extended: false })
Full example
import { http, body } from "@aex/core";
class User {
@http("post", "/user/login")
@body("urlencoded", { extended: false })
login() {
const [, , scope] = arguments;
const { body } = scope;
}
@http("post", "/user/logout")
@body()
login() {
const [, , scope] = arguments;
const { body } = scope;
}
}
@query
Decorator @query will parse query for you. After decorated with @query you will have scope.query to use. req.query is available for compatible reasion, but it is discouraged.
class Query {
@http("get", "/profile/:id")
@query()
public async id(req: any, res: any, _scope: any) {
// get /profile/111?page=20
req.query.page;
// 20
}
}
3. Static file serving decorators
Aex provides @serve decorator and its alias @assets for static file serving.
Due to
staticis taken as a reserved word for javascript,staticis not supported.
@serve and @assets
They take only one parameter:
- url: the base url for your served files.
It is recursive, so place used with caution, don't put your senstive files under that folder.
import { serve } from "@aex/core";
class StaticFileServer {
protected name = "formdata";
@serve("/assets")
public async upload() {
// All your files and subdirectories are available for accessing.
return resolve(__dirname, "./fixtures");
}
@assets("/assets1")
public async upload() {
// All your files and subdirectories are available for accessing.
return resolve(__dirname, "./fixtures");
}
}
4. Session management decorators
Aex provides @session decorator for default cookie based session management.
Session in other format can be realized with decorator @inject .
@session
Decorator @session takes a store as the parameter. It is an object derived from the abstract class ISessionStore. which is defined like this:
export declare abstract class ISessionStore {
abstract set(id: string, value: any): any;
abstract get(id: string): any;
abstract destroy(id: string): any;
}
aex provides two default store: MemoryStore and RedisStore .
RedisStore can be configurated by passing options through its constructor. The passed options is of the same to the function createClient of the package redis . You can check the option details here
For MemoryStore , you can simply decorate with @session() .
For RedisStore , you can decorate with an RedisStore as @session(redisStore) . Be sure to keep the variable redisStore global, because sessions must share only one store.
// Must not be used @session(new RedisStore(options)).
// For sessions share only one store over every request.
// There must be only one object of the store.
const store = new RedisStore(options);
class Session {
@post("/user/login")
@session()
public async get() {
const [, , scope] = arguments;
const { session } = scope;
session.user = user;
}
@get("/user/profile")
@session()
public async get() {
const [, , scope] = arguments;
const { session } = scope;
const user = session.user;
res.end(JSON.stringify(user));
}
@get("/user/redis")
@session(store)
public async get() {
const [, , scope] = arguments;
const { session } = scope;
const user = session.user;
res.end(JSON.stringify(user));
}
}
Share only one store object over requests.
5. Data filtering and validation decorators
Aex provides @filter to filter and validate data for you.
@filter
Decorator @filter will filter body , params and query data for you, and provide fallbacks respectively for each invalid data processing.
If the filtering rules are passed, then you will get a clean data from scope.extracted.
You can access scope.extracted.body, scope.extracted.params and scope.extracted.query if you filtered them.
But still you can access req.body, req.query, req,params after filtered.
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
// scope.extracted.body.username
// scope.extracted.body.password
// scope.extracted is the filtered data
}
@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
}
}
6. Error definition decorators
Aex provides @error decorator for error definition
@error
Decorator @error will generate errors for you.
Reference errorable for detailed usage.
@error take two parameters exactly what function Generator.generate takes.
Besides you can add lang attribut to @error to default the language, this feature will be automatically removed by aex when generate errors.
With the lang attribute, you can new errors without specifying a language every time throw/create an error;
class User {
@http("post", "/error")
@error({
lang: "zh-CN",
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 [, , scope] = arguments;
const { error: e } = scope;
const { ILoveYou } = e;
throw new ILoveYou('en-US');
throw new ILoveYou('zh-CN');
throw new ILoveYou(); // You can ignore language becuase you are now use the default language.
res.end("User Error!");
}
}
7. Custome middleware decorators
Aex provides @inject decorator for middleware injection.
@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.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
...
}
}
Usage with no decorators
Create an Aex instance
const aex = new Aex();Create a Router
const router = new Router();Setup the option for handler
router.get("/", async () => { // request processing time started console.log(scope.time.stated); // processing time passed console.log(scope.time.passed); res.end("Hello Aex!"); });Use router as an middleware
aex.use(router.toMiddleware());Start the server
const port = 3000; const host = "localhost"; const server = await aex.start(port, host); // server === aex.server
Websocket support
Simple example
Create a
WebSocketServerinstanceconst 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 });
Middlewares
Middlewares are defined like this:
export type IAsyncMiddleware = (
req: Request,
res: Response,
scope?: Scope
) => Promise<boolean | undefined | null | void>;
They return promise. so they must be called with await or .then().
Global middlewares
Global middlewares are effective all over the http request process.
They can be added by aex.use function.
aex.use(async () => {
// process 1
// return false
});
aex.use(async () => {
// process 2
// return false
});
// ...
aex.use(async () => {
// process N
// return false
});
Return
falsein middlewares will cancel the whole http request processing
It normally happens after ares.end
Handler specific middlewares
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 () => {
res.end("Hello world!");
},
[
async () => {
// process 1
// return false
},
async () => {
// process 2
// return false
},
// ...,
async () => {
// process N
// return false
},
]
);
Websocket middlewares
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
Accessable members
server
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();
Scope
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 () => {
// 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.
time
Get the requesting time
scope.time.started;
// 2019-12-12T09:01:49.543Z
Get the passed time
scope.time.passed;
// 2019-12-12T09:01:49.543Z
outer and inner
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
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 () => {
const { debug } = scope;
const logger = debug("aex:scope");
logger("this is a debugging info");
};
all these build-in attribute are readonly
// 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!
Helpers
Helpers are special middlewares with parameters to ease some fixed pattern with web development.
They must work with decorator @inject.
The first aviable helper is paginate.
paginate
Helper paginate can help with your pagination data parsing and filtering. It gets the correct value for you, so you can save your code parsing and correcting the pagination data before using them.
paramters
paginate is function takes two parameter:
limitis the default value of limitation for pagination, if the request is not specified. This function defaults it to20, so you your request doesn't specific a value tolimit, it will be assigned20.typeis the type data you use for pagination, it can bebody,params,query.queryis the default one. Before usepaginate, you must parsed your data. For if you usebodyfor pagination, normally your reuqest handlers should be deocrated with@body. Becuase params are parsed internally, using params needs no docrator. The data to be parsed must contain two parameters which should be named with :page,limit. for typequery, pagination data can be :list?page=2&limit=30;
usage
After parsing, scope will be added a attribute pagination, which is a object have three attribute: page, limit, offset. so you can simply extract them with
const {
pagination: { page, limit, offset },
} = scope;
Here is how to use helper paginate.
class Paginator {
@http('/page/query')
@query()
@inject(paginate(10, 'query'))
async public pageWithQuery() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
@http('/page/body')
@body()
@inject(paginate(10, 'body'))
async public pageWithBody() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
@http('/page/params/:page/:limit')
@body()
@inject(paginate(10, 'body'))
async public pageWithParams() {
const [, , scope] = arguments;
const {pagination: {page, limit, offset}} = scope;
...
}
}
Use middlewares from expressjs
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.
Tests
npm install
npm test
Effective Versioning
Aex follows a general versioning called Effective Versioning.
All lives matter
Aex supports human equality in law of rights only and supports human diversity in other area. Humans will never be equal in most area which is law of nature.
Lincense
MIT