appex is a nodejs web application framework built around the TypeScript programming language and
compiler. appex lets developers create and route http endpoints with TypeScript modules and
functions, as well as providing nodejs developers similar reflection and type introspection
services found in platforms such as .net.
### create a application
The following code will create a standalone appex application and
http server and listen on port 3000.
var appex = require('appex');
var app = appex({ program : './program.ts',
devmode : true,
logging : true });
app.listen(3000);
note: devmode and logging are optional. however, when developing
with appex, it is helpful to have these enabled.
### running on an existing http server
The following demonstrates setting up appex on an existing nodejs http server. In
this example, appex will attempt to handle incoming requests, and if appex cannot
route the request, will fire the callback.
var http = require('http');
var appex = require('appex');
var app = appex({ program : './program.ts' });
var server = http.createServer(function(req, res){
app(req, res, function() {
});
});
server.listen(3000);
### running as express middleware
appex allows developers to augment existing express / connect applications by
way of middleware. The following demonstrates setting up appex as express middleware.
var express = require('express');
var appex = require('appex');
var app = express();
app.use( appex({ program : './program.ts' }) );
app.get('/', function(req, res) {
res.send('Hello World');
});
app.listen(3000);
Like in the "running on an existing http server" example above, appex will attempt to intercept incoming requests.
if appex cannot find a matching route for the request, it will automatically call the "next" function to pass the request
on to the next middleware or express handler.
in addition to this, appex may also act as traditional express middleware. In the example below, a appex wildcard
function is created which will match "all" incoming requests, the wildcard function simply prints hello world to
the console and then calls context.next(), which passes the request on the express handler.
export function wildcard(context, path) {
console.log('hello world!!');
context.next();
}
var express = require('express');
var appex = require('appex');
var app = express();
app.use( appex({ program : './program.ts' }) );
app.get('/', function(req, res) {
res.send('Hello World');
});
app.listen(3000);
Just like traditional express middleware, appex will also inheriate the characteristics of the request.
consider the following example in which the jade view engine is configured for use. appex will inheritate the
response.render() method, which is passed to the appex handler as context.response.render()
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use( appex({program:'./program.ts', devmode:true} ));
});
export function index(context) {
context.response.render('index', { title: 'Express' });
}
### app context
All appex functions are passed a application context object as their first argument. The app context object
encapulates the http request and response objects issued by the underlying http server, as well as
additional objects specific to appex. These are listed below:
export function method(context) {
}
it is possible to extend the default objects passed on the context by adding them on the appex startup options. The
following will attach the async module to the context.
var appex = require('appex');
var app = appex({ program : './program.ts',
devmode : true,
context: {
async : require('async')
}});
app.listen(3000);
export function index(context) {
context.response.send('home page');
}
### wildcard handlers
Wildcard handlers resolve their urls to their current module scope + url.
appex wildcard handlers allow for wildcard routing at a given module scope. Wildcard handlers
support 'typed' url argument mapping, as denoted by the arguments annotation.
In addition, wildcard handlers also support optional arguments which can be specified with TypeScript's '?'
on argument names.
appex wildcard handlers require the following signature:
- name - 'wildcard'
- argument[0] - app context
- argument[n] - 1 or more arguments to be mapped from the url
- returns - void (optional)
declare var console;
export module blogs {
export function wildcard(context, year:number, month:number, day?:number) {
context.response.json({ year: year, month: month, day: day})
}
}
export function index(context) {
context.response.send('home');
}
export function wildcard(context, path) {
context.response.send(404, 'not found');
}
note: appex supports boolean, number, string and any annotations on wildcard arguments. if no annotation
is specified, appex interprets the argument as a string. the type 'any' is also interpreted as string.
note: wildcard functions should be declared last in any module scope. this ensures other routes
will be matched first.
### cascades
appex supports a cascading attribute scheme on modules and functions. With this, developers can apply
arbituary meta data for modules and functions that will propagate through scope. appex has two special
cascade properties for middleware and http verb matching, which are described below, however consider
the following code which illustrates the concept.
declare function cascade (qualifier:string, obj:any);
cascade({a: 10});
cascade('foo', {b : 20})
export module foo {
cascade('foo.bar', {c : 30})
export module bar {
cascade('foo.bar.index', {d : 40})
export function index(context) {
context.response.json( context.cascade );
}
}
}
### http verbs
appex handles http verb matching with cascades. appex will recognise the
'verbs' property applied to the cascade to match against http verbs.
cascade('index', { verbs: ['get'] })
export function index (context) {
context.response.send('index')
}
cascade('index', { verbs: ['post', 'put'] })
export function submit (context) {
context.response.send('submit')
}
### middleware
appex supports middleware with cascades. appex middleware defined with cascades allows
developers to scope middleware on single functions, or entire module scopes. appex will
recognise the 'use' property applied to the cascade to invoke middleware.
the following demonstrates how one might use middleware to secure a site admin.
note: middleware 'must' call next or handle the request.
declare function cascade (qualifier:string, obj:any);
declare var console;
function authenticate(context) {
console.log('authenticate')
context.next();
}
function authorize(context) {
console.log('authorize')
context.next();
}
cascade('admin', {use: [authenticate, authorize]})
export module admin {
export function index(context) {
console.log(context.cascade);
context.response.send('access granted!')
}
}
export function index (context) {
console.log(context.cascade);
context.response.send('home')
}
### exporting functions
appex will only route functions prefixed with the TypeScript 'export' declarer. This rule
also applied to modules. Developers can use this to infer notions of public and private
at the http level.
consider the following example:
module private_module {
export function public_method () { }
function private_method() { }
}
function private_function() { }
export function public_function (context) {
private_function();
private_module.public_method();
context.response.send('public_function');
}
## serving static files
Use wildcard functions with context.response.serve() to serve static content.
export module static {
export function wildcard(context, path) {
context.response.serve('./static/', path);
}
}
export function index (context) {
context.response.send('home page');
}
export function wildcard(context, path) {
context.response.send(404, path + ' not found');
}
### overview
The appex template engine is available to all handlers by default. it is accessible
on the context.template property. the following is an example of its use.
//----------------------------------------------
// view.txt
//----------------------------------------------
<ul>
@for(var n in context.users) {
@if(context.users[n].online) {
<li>@(context.users[n].name)</li>
}
}
</ul>
//----------------------------------------------
// program.ts
//----------------------------------------------
export function index(context) {
var users = [{name:'dave' , online : true},
{name:'smith', online : true},
{name:'jones', online : false},
{name:'alice', online : true}];
var text = context.template.render('./view.txt', { users: users });
context.response.headers['Content-Type'] = 'text/html';
context.response.send(text);
}
### context
each template is passed a data context. this context allows the caller to
send data to the template for rendering. the context parameter is optional.
the example below is sending the users array to the template context for
rendering.
export function index(context) {
var users = [{name:'dave' , online : true},
{name:'smith', online : true},
{name:'jones', online : false},
{name:'alice', online : true}];
context.response.send(context.template.render('./view.txt', { users: users }));
}
### syntax
appex templates support the following statements and syntax
if statement
if statments are supported.
@if(expression) {
some content
}
@if(a > 10) {
some content
}
@(user.loggedin) {
<span>welcome</span>
}
for statement
the following for loops are supported.
@for(var i = i; i < 100; i++) {
@(i)
}
@for(var n in list) {
@(list[n])
}
expressions
will emit the value contained.
@('hello world')
@(123)
@(some_variable)
code blocks
code blocks can be useful for adding template side rendering logic.
@{
var message = 'hello'
}
@(message)
@*
this comment will not be rendered!
*@
### layouts and sections
appex templates support template inheritance.
consider the following where layout.txt defines the sections 'header' and 'content' and the view.txt overrides
these sections with its own content.
//----------------------------------------------
// layout.txt
//----------------------------------------------
<html>
<head>
@section header
</head>
<body>
@section content {
<span>some default content</span>
}
</body>
</html>
//----------------------------------------------
// view.txt
//----------------------------------------------
@layout 'layout.txt'
@section header {
<title>my page</title>
}
@section content {
<p>overriding the layout.txt content section.</p>
<ul>
@for(var n in context.users) {
@if(context.users[n].online) {
<li>@(context.users[n].name)</li>
}
}
</ul>
}
//----------------------------------------------
// program.ts
//----------------------------------------------
export function index(context) {
var users = [{name:'dave' , online : true},
{name:'smith', online : true},
{name:'jones', online : false},
{name:'alice', online : true}];
var text = context.template.render('./view.txt', { users: users });
context.response.send(text);
}
note : when specifying a layout, the view will only render content within
the layouts section placeholders.
### render
appex templates also allow for partial views with the @render statment. consider the following
which renders the nav.txt file into the layout.txt file.
//----------------------------------------------
// nav.txt
//----------------------------------------------
<ul>
<li>home</li>
<li>about</li>
<li>contact</li>
</ul>
//----------------------------------------------
// layout.txt
//----------------------------------------------
<html>
<head>
@section header
</head>
<body>
@render 'nav.txt'
@section content {
<span>some default content</span>
}
</body>
</html>
//----------------------------------------------
// view.txt
//----------------------------------------------
@layout 'layout.txt'
@section header {
<title>my page</title>
}
@section content {
<p>overriding the layout.txt content section.</p>
<ul>
@for(var n in context.users) {
@if(context.users[n].online) {
<li>@(context.users[n].name)</li>
}
}
</ul>
}
//----------------------------------------------
// program.ts
//----------------------------------------------
export function index(context) {
var users = [{name:'dave' , online : true},
{name:'smith', online : true},
{name:'jones', online : false},
{name:'alice', online : true}];
var text = context.template.render('./view.txt', { users: users });
context.response.send(text);
}
### caching and devmode
appex template content is not cached (the implementor is expected to handle their own caching)
however the generated template code is.
appex templates do inheriate the behaviour of the appex 'devmode' option. setting
devmode to 'true' will cause template code to be reloaded from disk and code generated with each
request. setting devmode to false will load content from disk on first request, and
cache the generated template code in memory for the lifetime of the application.
in addition to this, a implementation where the devmode is false can override the caching
behaviour with the following.
export function index(context) {
context.template.option.devmode = true;
context.response.send(context.template.render('./view.txt'))
}
### generating schema
The following demonstrates generating json schema from the following class
hierarchy.
export module model {
export class Product {
public name : string;
public description : string;
public cost : number;
}
export class Order {
public products : Product;
}
export class Customer {
public firstname : string;
public lastname : string;
public orders : Order[];
}
}
export function index (context:appex.web.IContext) {
var schema = context.schema.generate('model.Customer');
context.response.json(schema);
}
which generates the following json schema.
{
"id": "model.Customer",
"type": "object",
"description": "a customer",
"properties": {
"firstname": {
"type": "string",
"description": "the customers firstname",
"required": true
},
"lastname": {
"type": "string",
"description": "the customers lastname",
"required": true
},
"orders": {
"type": "array",
"items": {
"type": {
"id": "model.Order",
"type": "object",
"description": "a order",
"properties": {
"products": {
"id": "model.Product",
"type": "object",
"description": "a product",
"properties": {
"name": {
"type": "string",
"description": "the product name",
"required": true
},
"description": {
"type": "string",
"description": "the product description",
"required": true
},
"cost": {
"type": "number",
"description": "the product cost",
"required": true
}
},
"required": true
}
}
}
},
"description": "orders made by this customer",
"required": true
}
}
}
a quick note...
when generating schema from classes:
- only public class variables will be emitted.
- all properties will be marked as "required".
when generating schema from interfaces:
- all properties will be emitted.
- all properties will be marked as "required" unless modified with '?'.
### validating json
appex supports json schema validation from class and interface definitions. consider the following...
interface Customer {
firstname : string;
lastname : string;
age : number;
emails : string[];
option_a ? : boolean;
option_b ? : boolean;
}
export function index(context) {
var customer = {
firstname : 'dave',
age : '33',
emails : [12345, 'dave@domain.com', true],
option_b : 1,
option_c : 1
}
var errors = context.schema.validate('Customer', customer);
if(errors) {
context.response.json(errors);
}
}
will output the following.
[
{
"message": "instance.lastname is required."
},
{
"message": "instance.age is not a number"
},
{
"message": "instance.emails[0] is not a string"
},
{
"message": "instance.emails[2] is not a string"
},
{
"message": "instance.option_b is not a boolean"
},
{
"message": "instance.option_c unexpected property"
}
]
### reflect specific types
In typical scenarios, developers will want to leverage reflection meta data to generate
service contacts and client side models. the reflection api lets you access meta data
for the following types declared in your project.
- modules
- imports
- classes
- interfaces
- functions
- variables
to access specific type metadata, use the reflection.get([qualifier]) method, as demonstrated below.
export module model {
export class Customer {
public firstname : string;
public lastname : string;
public age : number;
}
}
export function index (context:appex.web.Context) {
context.response.json( context.module.reflection.get('model.Customer') );
}
and methods..
function some_method(a:string, b:number, c?:boolean) : void { }
export function index (context:appex.web.Context) {
context.response.json( context.module.reflection.get('some_method') );
}
....and variables...
var some_variable:number = 10;
export function index (context:appex.web.Context) {
context.response.json( context.module.reflection.get('some_variable') );
}
### appex.d.ts declaration
If you develop on a TypeScript complicant editor (one that supports TS 0.9), appex comes bundled
with a declaration file you can reference in your project. If installing appex via npm, your
reference should be as follows.
export function index (context:appex.web.IContext) {
context.response.send('hello');
}
export function wildcard(context:appex.web.IContext, path:string) {
context.response.serve('./', path);
}
By referencing this in your project, you get the benefits of code completion and static type checking
against both appex, and the nodejs core.
### structuring projects
appex includes TypeScript's ability to reference source files with the 'reference' element. appex
will traverse each source files references and include it as part of the compilation.
Developers can use this functionality to logically split source files into reusable components of
functionality, as demonstrated below.
var appex = require('appex');
var app = appex ({ program : './index.ts' });
app.listen(3000);
export module users {
export function login (context) { context.response.send('users.login') }
export function logout (context) { context.response.send('users.logout') }
}
export function index (context) { context.response.send('home') }
export function about (context) { context.response.send('about') }
export function contact (context) { context.response.send('contact') }
export function wildcard (context, path) { context.response.send(404, ' not found') }
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.