tyx-synergi
v0.3.77
Published
TyX Core Framework, Serverless back-end in TypeScript for AWS Lambda
Downloads
15
Keywords
Readme
TyX Core Framework
Serverless back-end framework in TypeScript for AWS Lambda. Declarative dependency injection and event binding.
Table of Contents
- 1. Installation
- 2. Examples of Usage
- 3. Concepts Overview
- 4. Data Structures
- 5. Service Decorators
- 6. HTTP Decorators
- 7. Method Argument Decorators
- 8. Authorization Decorators
- 9. Interfaces and Classes
1. Installation
Install module:
npm install tyx --save
reflect-metadata
shim is required:
npm install reflect-metadata --save
and make sure to import it before you use tyx:
import "reflect-metadata";
Its important to set these options in tsconfig.json
file of your project:
{
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
2. Examples of Usage
The following examples are constructed to cover all features of TyX.
TODO: How to build and run the examples
2.1. Simple service
The most basic use scenario is a REST service. It is not required to inherit any base classes; use of the provided decorators is sufficient to bind service methods to corresponding HTTP methods and paths: @Get
, @Post
, @Put
, @Delete
. Method arguments bind to request elements using decorators as well; @QueryParam
, @PathParam
and @Body
are the core binding.
Use of @Service()
decorator is mandatory to mark the class as service and enable proper collection of metadata emitted from decorators.
As security consideration TyX does not default to public access for service methods, @Public()
decorator must be explicitly provided otherwise the HTTP binding is effectively disabled.
For simplicity in this example all files are in the same folder package.json
, service.ts
, function.ts
, local.ts
and serverless.yml
Service implementation
import { Service, Public, PathParam, QueryParam, Body, Get, Post, Put, Delete } from "tyx";
@Service()
export class NoteService {
@Public()
@Get("/notes")
public getAll(@QueryParam("filter") filter?: string) {
return { action: "This action returns all notes", filter };
}
@Public()
@Get("/notes/{id}")
public getOne(@PathParam("id") id: string) {
return { action: "This action returns note", id };
}
@Public()
@Post("/notes")
public post(@Body() note: any) {
return { action: "Saving note...", note };
}
@Public()
@Put("/notes/{id}")
public put(@PathParam("id") id: string, @Body() note: any) {
return { action: "Updating a note...", id, note };
}
@Public()
@Delete("/notes/{id}")
public remove(@PathParam("id") id: string) {
return { action: "Removing note...", id };
}
}
Lambda function
Services are plain decorated classes unaware of the specifics of AWS Lambda, the provided LambdaContainer
class takes care of managing the service and dispatching the trigger events. The container export()
provides the handler
entry point for the lambda function.
import { LambdaContainer, LambdaHandler } from "tyx";
import { NoteService } from "./service";
// Creates an Lambda container and publish the service.
let container = new LambdaContainer("tyx-sample1")
.publish(NoteService);
// Export the lambda handler function
export const handler: LambdaHandler = container.export();
Express container
For local testing developers can use the ExpressContainer
class, it exposes routes based on service method decorations. When run in debug mode from an IDE (such as VS Code) it allows convenient debugging experience.
import { ExpressContainer } from "tyx";
import { NoteService } from "./service";
// Creates an Express container and publish the service.
let express = new ExpressContainer("tyx-sample1")
.publish(NoteService);
// Start express server
express.start(5000);
Open in browser http://localhost:5000/notes
or http://localhost:5000/notes/1
.
Serverless file
Serverless Framework is used to package and deploy functions developed in TyX. Events declared in serverless.yml
should match those exposed by services published in the function LambdaContainer
. Missing to declare the event will result in ApiGateway rejecting the request, having events (paths) not bound to any service method will result in the container rejecting the request.
service: tyx-sample1
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 5
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
LOG_LEVEL: INFO
functions:
notes-function:
handler: function.handler
events:
- http:
path: notes
method: GET
cors: true
- http:
path: notes/{id}
method: GET
cors: true
- http:
path: notes/{id}
method: POST
cors: true
- http:
path: notes/{id}
method: PUT
cors: true
- http:
path: notes/{id}
method: DELETE
cors: true
2.2. Dependency injection
It is possible write an entire application as single service but this is rarely justified. It make sense to split the application logic into multiple services each encapsulating related actions and responsibilities.
This example has a more elaborate structure, separate service API definition and implementation using dependency injection. Two of of the services are for private use within the same container not exposing any event bindings.
The folder structure used starting with this example:
api/
scripts with service API definitionservices/
implementationsfunctions/
scripts withLambdaContainer
exporting a handler functionlocal/
local run usingExpressContainer
serverless.yml
Serverless Framework
Following examples will further build on this to split services into dedicated functions and then into separate applications (Serverless projects).
API definition
TypeScript interfaces have no corresponding representation once code compiles to JavaScript; so to use the interface as service identifier it is also declared and exported as a constant. This is allowed as TypeScript supports declaration merging.
The services API are returning Promises, this should be a default practice as real life service implementations will certainly use external libraries that are predominantly asynchronous. TypeScript support for async
and await
makes the code concise and clean.
api/box.ts
export const BoxApi = "box";
export interface BoxApi {
produce(type: string): Promise<Box>;
}
export interface Box {
service: string;
id: string;
type: string;
}
api/item.ts
export const ItemApi = "item";
export interface ItemApi {
produce(type: string): Promise<Item>;
}
export interface Item {
service: string;
id: string;
name: string;
}
api/factory.ts
import { Box } from "./box";
import { Item } from "./item";
export const FactoryApi = "factory";
export interface FactoryApi {
produce(boxType: string, itemName: string): Promise<Product>;
}
export interface Product {
service: string;
timestamp: string;
box: Box;
item: Item;
}
Services implementation
Interfaces are used as service names @Service(BoxApi)
and @Inject(ItemApi)
as well as types of the injected properties (dependencies). Dependencies are not part of the service API but its implementation. TypeScript access modifiers (public
, private
, protected
) are not enforced in runtime so injected properties can be declared protected
as a convention choice.
Imports of API declarations are skipped in the code samples.
services/box.ts
@Service(BoxApi)
export class BoxService implements BoxApi {
private type: string;
constructor(type: string) {
this.type = type || "default";
}
@Private()
public async produce(type: string): Promise<Box> {
return {
service: ServiceMetadata.service(this),
id: Utils.uuid(),
type: type || this.type
};
}
}
services/item.ts
@Service(ItemApi)
export class ItemService implements ItemApi {
@Private()
public async produce(name: string): Promise<Item> {
return {
service: ServiceMetadata.service(this),
id: Utils.uuid(),
name
};
}
}
The @Private()
declaration documents that the methods are intended for invocation within the container.
services/factory.ts
@Service(FactoryApi)
export class FactoryService implements FactoryApi {
@Inject(BoxApi)
protected boxProducer: BoxApi;
@Inject(ItemApi)
protected itemProducer: ItemApi;
@Public()
@Get("/product")
public async produce(@QueryParam("box") boxType: string, @QueryParam("item") itemName: string): Promise<Product> {
let box: Box = await this.boxProducer.produce(boxType);
let item: Item = await this.itemProducer.produce(itemName || "item");
return {
service: ServiceMetadata.service(this),
timestamp: new Date().toISOString(),
box,
item
};
}
}
Lambda function
The container can host multiple services but only those provided with publish()
are exposed for external requests. Both register()
and publish()
should be called with the service constructor function (class), followed by any constructor arguments if required.
let container = new LambdaContainer("tyx-sample2")
// Internal services
.register(BoxService, "simple")
.register(ItemService)
// Public service
.publish(FactoryService);
export const handler: LambdaHandler = container.export();
Express container
let express = new ExpressContainer("tyx-sample2")
// Internal services
.register(BoxService, "simple")
.register(ItemService)
// Public service
.publish(FactoryService);
express.start(5000);
Serverless file
The Serverless file is only concerned with Lambda functions not the individual TyX services within those functions.
service: tyx-sample2
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 5
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
LOG_LEVEL: INFO
functions:
factory-function:
handler: functions/factory.handler
events:
- http:
path: product
method: GET
cors: true
2.3. Function per service
Decoupling the service API and implementation in the previous example allows to split the services into their own dedicated functions. This is useful when service functions need to have fine tuned settings; starting from the basic, memory and timeout, environment variables (configuration) up to IAM role configuration.
When deploying services in dedicated functions service-to-service communication is no longer a method call inside the same Node.js process. To allow transparent dependency injection TyX provides for proxy service implementation that using direct Lambda to Lambda function invocations supported by AWS SDK.
API definition
Identical to example 2.2. Dependency injection
Services implementation
The only difference from previous example is that BoxService
and ItemService
have their method decorated with @Remote()
instead of @Private()
. This allows the method to be called outside of the host Lambda functions.
services/box.ts
@Service(BoxApi)
export class BoxService implements BoxApi {
private type: string;
constructor(type: string) {
this.type = type || "default";
}
@Remote()
public async produce(type: string): Promise<Box> {
return {
service: ServiceMetadata.service(this),
id: Utils.uuid(),
type: type || this.type
};
}
}
services/item.ts
@Service(ItemApi)
export class ItemService implements ItemApi {
@Remote()
public async produce(name: string): Promise<Item> {
return {
service: ServiceMetadata.service(this),
id: Utils.uuid(),
name
};
}
}
services/factory.ts
@Service(FactoryApi)
export class FactoryService implements FactoryApi {
@Inject(BoxApi)
protected boxProducer: BoxApi;
@Inject(ItemApi)
protected itemProducer: ItemApi;
@Public()
@Get("/product")
public async produce(@QueryParam("box") boxType: string, @QueryParam("item") itemName: string): Promise<Product> {
let box: Box = await this.boxProducer.produce(boxType);
let item: Item = await this.itemProducer.produce(itemName || "item");
return {
service: ServiceMetadata.service(this),
timestamp: new Date().toISOString(),
box,
item
};
}
}
Proxy implementation
The provided LambdaProxy
class takes care of invoking the remote function and converting back the received result or error thrown.
The @Proxy
decorator is mandatory and requires at minimum the name of the proxied service.
The full signature however is @Proxy(service: string, application?: string, functionName?: string)
, when not provided application
defaults to the identifier specified in LambdaContainer
constructor; functionName
defaults to {service}-function
, in this example box-function
and item-function
respectively.
proxies/box.ts
@Proxy(BoxApi)
export class BoxProxy extends LambdaProxy implements BoxApi {
public async produce(type: string): Promise<Box> {
return this.proxy(this.produce, arguments);
}
}
- `proxies/item.ts`
@Proxy(ItemApi)
export class ItemProxy extends LambdaProxy implements ItemApi {
public async produce(name: string): Promise<Item> {
return this.proxy(this.produce, arguments);
}
}
Lambda functions
The argument passed to LambdaContainer
is application id and should correspond to the service
setting in serverless.yml
. Function-to-function requests within the same application are considered internal, while between different applications as remote. TyX has an authorization mechanism that distinguishes these two cases requiring additional settings. This example is about internal calls, next one covers remote calls. When internal function-to-function calls are used INTERNAL_SECRET
configuration variable must be set; this is a secret key that both the invoking and invoked function must share so LambdaContainer
can authorize the requests.
- Box function
let container = new LambdaContainer("tyx-sample3")
.publish(BoxService, "simple");
export const handler: LambdaHandler = container.export();
- Item function
let container = new LambdaContainer("tyx-sample3")
.publish(ItemService);
export const handler: LambdaHandler = container.export();
- Factory function
let container = new LambdaContainer("tyx-sample3")
// Use proxy instead of service implementation
.register(BoxProxy)
.register(ItemProxy)
.publish(FactoryService);
export const handler: LambdaHandler = container.export();
Express container
The following code allows to execute the FactoryService
in the local container while the proxies will interact with the deployed functions on AWS.
The provided config.ts
provides the needed environment
variables defined in serverless.yml
.
local/main.ts
import { Config } from "./config";
// Required for accessing Lambda via proxy on AWS
import AWS = require("aws-sdk");
AWS.config.region = "us-east-1";
let express = new ExpressContainer("tyx-sample3")
.register(DefaultConfiguration, Config)
.register(BoxProxy)
.register(ItemProxy)
.publish(FactoryService);
express.start(5000);
local/config.ts
export const Config = {
STAGE: "tyx-sample3-demo",
INTERNAL_SECRET: "7B2A62EF85274FA0AA97A1A33E09C95F",
LOG_LEVEL: "INFO"
};
Serverless file
Since internal function-to-function requests are use INTERNAL_SECRET
is set; this should be an application specific random value (e.g. UUID).
The additional setting REMOTE_SECRET_TYX_SAMPLE4
is to allow remote requests from the next example.
It is necessary to allow the IAM role to lambda:InvokeFunction
, of course it is recommended to be more specific about the Resource than in this example.
service: tyx-sample3
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
INTERNAL_SECRET: 7B2A62EF85274FA0AA97A1A33E09C95F
REMOTE_SECRET_TYX_SAMPLE4: D718F4BBCC7345749378EF88E660F701
LOG_LEVEL: DEBUG
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "arn:aws:lambda:${opt:region, self:provider.region}:*:*"
functions:
box-function:
handler: functions/box.handler
item-function:
handler: functions/item.handler
factory-function:
handler: functions/factory.handler
events:
- http:
path: product
method: GET
cors: true
2.4. Remote service
Building upon the previous example this one demonstrates a remote request via LambdaProxy
. The request is considered remote because involved services are deployed as separate serverless project - the previous example.
When remote function-to-function calls are used REMOTE_SECRET_(APPID)
and REMOTE_STAGE_(APPID)
configuration variables must be set. The first is a secret key that both the invoking and invoked function must share so LambdaContainer
can authorize the calls; the second is (service)-(stage)
prefix that Serverless Framework prepends to function names by default.
API definition
Identical to example 2.2. Dependency injection
Services implementation
BoxApi
and ItemApi
are not implemented as services in this example project, those provided and deployed by the previous example will be used via function-to-function calls.
@Service(FactoryApi)
export class FactoryService implements FactoryApi {
@Inject(BoxApi, "tyx-sample3")
protected boxProducer: BoxApi;
@Inject(ItemApi, "tyx-sample3")
protected itemProducer: ItemApi;
@Public()
@Get("/product")
public async produce(@QueryParam("box") boxType: string, @QueryParam("item") itemName: string): Promise<Product> {
let box: Box = await this.boxProducer.produce(boxType);
let item: Item = await this.itemProducer.produce(itemName || "item");
return {
service: ServiceMetadata.service(this),
timestamp: new Date().toISOString(),
box,
item
};
}
}
Proxy implementation
The second parameter of @Proxy()
decorator is provided as the target service is not in this serverless project.
@Proxy(BoxApi, "tyx-sample3")
export class BoxProxy extends LambdaProxy implements BoxApi {
public async produce(type: string): Promise<Box> {
return this.proxy(this.produce, arguments);
}
}
@Proxy(ItemApi, "tyx-sample3")
export class ItemProxy extends LambdaProxy implements ItemApi {
public async produce(name: string): Promise<Item> {
return this.proxy(this.produce, arguments);
}
}
Lambda function
Only the factory service is exposed as a function.
let container = new LambdaContainer("tyx-sample4")
// Use proxy instead of service implementation
.register(BoxProxy)
.register(ItemProxy)
.publish(FactoryService);
export const handler: LambdaHandler = container.export();
Express container
import { Config } from "./config";
// Required for accessing Lambda via proxy on AWS
import AWS = require("aws-sdk");
AWS.config.region = "us-east-1";
let express = new ExpressContainer("tyx-sample4")
.register(DefaultConfiguration, Config)
.register(BoxProxy)
.register(ItemProxy)
.publish(FactoryService);
express.start(5000);
Serverless file
The environment variables provide the remote secret and stage for application tyx-sample3
. In the previous example there is a matching REMOTE_SECRET_TYX_SAMPLE4
with the same value as REMOTE_SECRET_TYX_SAMPLE3
here, this pairs the applications. When a remote request is being prepared the secret for the target application is being used; when a remote request is received the secret corresponding to the requesting application is used to authorize the request.
service: tyx-sample4
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
REMOTE_SECRET_TYX_SAMPLE3: D718F4BBCC7345749378EF88E660F701
REMOTE_STAGE_TYX_SAMPLE3: tyx-sample3-demo
LOG_LEVEL: DEBUG
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "arn:aws:lambda:${opt:region, self:provider.region}:*:*"
functions:
factory-function:
handler: functions/factory.handler
events:
- http:
path: product
method: GET
cors: true
2.5. Authorization
TyX supports role-based authorization allowing to control access on service method level. There is no build-in authentication support, for the purpose of this example a hard-coded login service is used. The authorization is using JSON Web Token that are issued and validated by a build-in Security
service. The container instantiate the default security service if non is registered and use it to validate all requests to non-public service methods.
Apart from the @Public()
permission decorator @Query<R>()
and @Command<R>()
are provided to decorate service methods reflecting if the execution results in data retrieval or manipulation (changes).
API definition
api/app.ts
Definition of application roles interface, used as generic parameter of permission decorators.
import { Roles } from "tyx";
export interface AppRoles extends Roles {
Admin: boolean;
Manager: boolean;
Operator: boolean;
}
api/login.ts
Login service API
export const LoginApi = "login";
export interface LoginApi {
login(userId: string, password: string): Promise<string>;
}
api/factory.ts
Extended factory API
export const FactoryApi = "factory";
export interface FactoryApi {
// Admin only
reset(userId: string): Promise<Response>;
createProduct(userId: string, productId: string, name: string): Promise<Confirmation>;
removeProduct(userId: string, productId: string): Promise<Confirmation>;
// Admin & Manager
startProduction(userId: string, role: string, productId: string, order: any): Promise<Confirmation>;
stopProduction(userId: string, role: string, productId: string, order: any): Promise<Confirmation>;
// Operator
produce(userId: string, role: string, productId: string): Promise<Item>;
// Public
status(userId: string, role: string): Promise<Status>;
}
export interface Response {
userId: string;
role: string;
status: string;
}
export interface Product {
productId: string;
name: string;
creator: string;
production: boolean;
}
export interface Confirmation extends Response {
product: Product;
order?: any;
}
export interface Item extends Response {
product: Product;
itemId: string;
timestamp: string;
}
export interface Status extends Response {
products: Product[];
}
Login implementation
The login service provides a public entry point for users to obtain an access token. The injected Security
service is always present in the container.
@Service(LoginApi)
export class LoginService implements LoginApi {
@Inject(Security)
protected security: Security;
@Public()
@Post("/login")
@ContentType("text/plain")
public async login(
@BodyParam("userId") userId: string,
@BodyParam("password") password: string): Promise<string> {
let role: string = undefined;
switch (userId) {
case "admin": role = password === "nimda" && "Admin"; break;
case "manager": role = password === "reganam" && "Manager"; break;
case "operator": role = password === "rotarepo" && "Operator"; break;
}
if (!role) throw new Unauthorized("Unknown user or invalid password");
return await this.security.issueToken({ subject: "user:internal", userId, role });
}
}
Service implementation
Methods reset()
, createProduct()
and removeProduct()
are decorated with @Command<AppRoles>({ Admin: true, Manager: false, Operator: false })
allowing only Admin users to invoke them via the HTTP bindings specified with @Post()
and @Delete()
decorators.
Methods startProduction()
and stopProduction()
are allowed to Admin and Manager role; both bind to the same path @Put("/product/{id}", true)
however the second param set to true
instructs the container that Content-Type
header will have an additional parameter domain-model
equal to the method name so to select the desired action, e.g. Content-Type: application/json;domain-model=startProduction
.
Method produce()
is allowed for all three roles but not for public access. Public access is allowed to method status()
with @Query<AppRoles>({ Public: true, Admin: true, Manager: true, Operator: true })
.
When the userId, role or other authorization attributes are needed in method logic @ContextParam("auth.{param}")
can be used to bind the arguments. Other option is to use @ContextObject()
and so get the entire context object as single attribute.
Having a service state
products
is for example purposes only, Lambda functions must persist the state in cloud services (e.g. DynamoDB).
@Service(FactoryApi)
export class FactoryService implements FactoryApi {
private products: Record<string, Product> = {};
// Admin only
@Command<AppRoles>({ Admin: true, Manager: false, Operator: false })
@Post("/reset")
public async reset(
@ContextParam("auth.userId") userId: string): Promise<Response> {
this.products = {};
return { userId, role: "Admin", status: "Reset" };
}
@Command<AppRoles>({ Admin: true, Manager: false, Operator: false })
@Post("/product")
public async createProduct(
@ContextParam("auth.userId") userId: string,
@BodyParam("id") productId: string,
@BodyParam("name") name: string): Promise<Confirmation> {
if (this.products[productId]) throw new BadRequest("Duplicate product");
let product = { productId, name, creator: userId, production: false, orders: [] };
this.products[productId] = product;
return { userId, role: "Admin", status: "Create product", product };
}
@Command<AppRoles>({ Admin: true, Manager: false, Operator: false })
@Delete("/product/{id}")
public async removeProduct(
@ContextParam("auth.userId") userId: string,
@PathParam("id") productId: string): Promise<Confirmation> {
let product = this.products[productId];
if (!product) throw new NotFound("Product not found");
delete this.products[productId];
return { userId, role: "Admin", status: "Remove product", product };
}
// Admin & Manager
@Command<AppRoles>({ Admin: true, Manager: true, Operator: false })
@Put("/product/{id}", true)
public async startProduction(
@ContextParam("auth.userId") userId: string,
@ContextParam("auth.role") role: string,
@PathParam("id") productId: string,
@Body() order: any): Promise<Confirmation> {
let product = this.products[productId];
if (!product) throw new NotFound("Product not found");
product.production = true;
product.orders.push(order);
return { userId, role, status: "Production started", product, order };
}
@Command<AppRoles>({ Admin: true, Manager: true, Operator: false })
@Put("/product/{id}", true)
public async stopProduction(
@ContextParam("auth.userId") userId: string,
@ContextParam("auth.role") role: string,
@PathParam("id") productId: string,
@Body() order: any): Promise<Confirmation> {
let product = this.products[productId];
if (!product) throw new NotFound("Product not found");
product.production = false;
product.orders.push(order);
return { userId, role, status: "Production stopped", product, order };
}
// + Operator
@Command<AppRoles>({ Admin: true, Manager: true, Operator: true })
@Get("/product/{id}")
public async produce(
@ContextParam("auth.userId") userId: string,
@ContextParam("auth.role") role: string,
@PathParam("id") productId: string): Promise<Item> {
let product = this.products[productId];
if (!product) throw new NotFound("Product not found");
if (!product.production) throw new BadRequest("Product not in production");
let item: Item = {
userId, role,
status: "Item produced",
product,
itemId: Utils.uuid(),
timestamp: new Date().toISOString()
};
return item;
}
@Query<AppRoles>({ Public: true, Admin: true, Manager: true, Operator: true })
@Get("/status")
public async status(
@ContextParam("auth.userId") userId: string,
@ContextParam("auth.role") role: string): Promise<Status> {
let products = [];
Object.keys(this.products).forEach(k => products.push(this.products[k]));
return { userId, role, status: "Status", products };
}
}
Lambda functions
Login and Factory services are independent so can be deployed as separate functions.
services/factory.ts
let container = new LambdaContainer("tyx-sample5")
.publish(FactoryService);
export const handler: LambdaHandler = container.export();
services/login.ts
let container = new LambdaContainer("tyx-sample5")
.publish(LoginService);
export const handler: LambdaHandler = container.export();
Express container
The container constructor accepts an additional argument that is path prefix for all exposed routes. In this case /demo
match how Api Gateway by default will include the stage name in the path.
import { Config } from "./config";
let express = new ExpressContainer("tyx-sample5", "/demo")
.register(DefaultConfiguration, Config)
.publish(LoginService)
.publish(FactoryService);
express.start(5000);
Serverless file
When using authorization it is necessary to provide a HTTP_SECRET
that is used to sign and verify the web tokens as well as HTTP_TIMEOUT
how long the tokens are valid.
service: tyx-sample5
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
HTTP_SECRET: 3B2709157BD8444BAD42DE246D41BB35
HTTP_TIMEOUT: 2h
LOG_LEVEL: DEBUG
functions:
login-function:
handler: functions/login.handler
events:
- http:
path: login
method: POST
cors: true
factory-function:
handler: functions/factory.handler
events:
- http:
path: reset
method: POST
cors: true
- http:
path: product
method: POST
cors: true
- http:
path: product/{id}
method: DELETE
cors: true
- http:
path: product/{id}
method: PUT
cors: true
- http:
path: product/{id}
method: GET
cors: true
- http:
path: status
method: GET
cors: true
Token sample
When posting to example LoginService
at /demo/login
with json body { userId: "admin", password: "nimda" }
a token is received as plain text:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJvaWQiOiJhZG1pbiIsInJvbGUiOiJBZG1pbiIsImlhdCI6MTUwODk0NDI5NywiZX
hwIjoxNTA4OTUxNDk3LCJhdWQiOiJ0eXgtc2FtcGxlNSIsImlzcyI6InR5eC1zYW1w
bGU1Iiwic3ViIjoidXNlcjppbnRlcm5hbCIsImp0aSI6ImI4N2U1MDYyLTYwNjItND
k0Ny1iMTU1LWZmNzA0NzBhMTEzZCJ9.
b8H27N26QKFbFofuMPd1PGQHG7UeB5J1FIoQIte-dss
Decoded it contains the minimum info for the security service:
oid
is user identifierrole
application roleiat
issued-at timestampexp
expiry timestampaud
application id the token is intended toiss
application id issuing the tokensub
subject / token typejti
unique token id
{
"alg": "HS256",
"typ": "JWT"
}
{
"oid": "admin",
"role": "Admin",
"iat": 1508944297,
"exp": 1508951497,
"aud": "tyx-sample5",
"iss": "tyx-sample5",
"sub": "user:internal",
"jti": "b87e5062-6062-4947-b155-ff70470a113d"
}
2.6. Express service
Express is an established node.js web framework and there is a wealth of third party middleware packages that may not be available in other form. The ExpressService
base class uses aws-serverless-express to host an Express application. This is not intended to host existing Express applications but more as a solution to bridge the gap for specific functionalities, for example use Passport.js to implement user authentication.
API definition
Service methods delegating to Express must have a signature method(ctx: Context, req: HttpRequest): Promise<HttpResponse>
, the service can have ordinary methods as well.
export const ExampleApi = "example";
export interface ExampleApi {
hello(): string;
onGet(ctx: Context, req: HttpRequest): Promise<HttpResponse>;
onPost(ctx: Context, req: HttpRequest): Promise<HttpResponse>;
other(ctx: Context, req: HttpRequest): Promise<HttpResponse>;
}
Service implementation
Multiple routes delegated to Express for processing can be declared with decorators over a single method, such as other()
in the example or over dedicated methods such as onGet()
and onPost()
to allow for logic preceding or following the Express processing. Presence of @ContentType("RAW")
is required to pass the result verbatim to the user (statusCode
, headers
, body
as generated by Express) otherwise the returned object will be treated as a json body.
The base class requires to implement the abstract method setup(app: Express, ctx: Context, req: HttpRequest): void
that setup the Express app to be used for request processing. Each instance of the express app is used for a single request, no state can be maintained inside Lambda functions.
import BodyParser = require("body-parser");
@Service(ExampleApi)
export class ExampleService extends ExpressService implements ExampleApi {
@Public()
@Get("/hello")
@ContentType("text/plain")
public hello(): string {
return "Express service ...";
}
@Public()
@Get("/app")
@ContentType("RAW")
public async onGet(@ContextObject() ctx: Context, @RequestObject() req: HttpRequest): Promise<HttpResponse> {
return super.process(ctx, req);
}
@Public()
@Post("/app")
@ContentType("RAW")
public async onPost(@ContextObject() ctx: Context, @RequestObject() req: HttpRequest): Promise<HttpResponse> {
return super.process(ctx, req);
}
@Public()
@Put("/app")
@Delete("/app/{id}")
@ContentType("RAW")
public async other(@ContextObject() ctx: Context, @RequestObject() req: HttpRequest): Promise<HttpResponse> {
return super.process(ctx, req);
}
protected setup(app: Express, ctx: Context, req: HttpRequest): void {
app.register(BodyParser.json());
app.get("/app", (xreq, xres) => this.flush(xreq, xres, ctx, req));
app.post("/app", (xreq, xres) => this.flush(xreq, xres, ctx, req));
app.put("/app", (xreq, xres) => this.flush(xreq, xres, ctx, req));
app.delete("/app/:id", (xreq, xres) => this.flush(xreq, xres, ctx, req));
}
private flush(xreq: Request, xres: Response, ctx: Context, req: HttpRequest) {
let result = {
msg: `Express ${req.method} method`,
path: xreq.path,
method: xreq.method,
headers: xreq.headers,
params: xreq.params,
query: xreq.query,
body: xreq.body,
lambda: { ctx, req }
};
xres.send(result);
}
}
Lambda function
let container = new LambdaContainer("tyx-sample6")
.publish(ExampleService);
export const handler: LambdaHandler = container.export();
Express container
Applications containing express services can be run with the ExpressContainer
, the container express instance and the internal service instance remain completely separate.
let express = new ExpressContainer("tyx-sample6")
.publish(ExampleService);
express.start(5000);
Serverless file
There no special requirements for functions hosting Express services.
service: tyx-sample6
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
LOG_LEVEL: DEBUG
functions:
example-function:
handler: functions/example.handler
events:
- http:
path: hello
method: GET
cors: true
- http:
path: app
method: GET
cors: true
- http:
path: app
method: POST
cors: true
- http:
path: app
method: PUT
cors: true
- http:
path: app/{id}
method: DELETE
cors: true
2.7. Error handling
Error handling is implemented in TyX containers to ensure both explicitly thrown errors and runtime errors are propagated in unified format to the calling party. Classes corespnding to standard HTTP error responses are provided:
400 BadRequest
401 Unauthorized
403 Forbidden
404 NotFound
409 Conflict
500 InternalServerError
501 NotImplemented
503 ServiceUnavailable
API definition
api/calculator.ts
combined service
export const CalculatorApi = "calculator";
export interface CalculatorApi {
mortgage(amount: any, nMonths: any, interestRate: any, precision: any): Promise<MortgageResponse>;
missing(req: any): Promise<number>;
unhandled(req: any): Promise<number>;
}
export interface MortgageResponse {
monthlyPayment: number;
total: number;
totalInterest: number;
}
api/mortgage.ts
mortgage calculation service
export const MortgageApi = "mortgage";
export interface MortgageApi {
calculate(amount: number, nMonths: number, interestRate: number, precision: number): Promise<number>;
}
api/missing.ts
used for proxy to missing function
export const MissingApi = "missing";
export interface MissingApi {
calculate(req: any): Promise<number>;
}
api/missing.ts
used for proxy to a function throwing unhandled exception
export const UnhandledApi = "unhandled";
export interface UnhandledApi {
calculate(req: any): Promise<number>;
}
Services implementation
services/calculator.ts
validates that body parameters are present and expected type.
@Service(CalculatorApi)
export class CalculatorService implements CalculatorApi {
@Inject(MortgageApi)
protected mortgageService: MortgageApi;
@Inject(MissingApi)
protected missingService: MissingApi;
@Inject(UnhandledApi)
protected unhandledService: UnhandledApi;
@Public()
@Post("/mortgage")
public async mortgage(@BodyParam("amount") amount: any,
@BodyParam("nMonths") nMonths: any,
@BodyParam("interestRate") interestRate: any,
@BodyParam("precision") precision: any): Promise<MortgageResponse> {
let _amount = Number.parseFloat(amount);
let _nMonths = Number.parseFloat(nMonths);
let _interestRate = Number.parseFloat(interestRate);
let _precision = precision && Number.parseFloat(precision);
// Type validation
let errors: ApiErrorBuilder = BadRequest.builder();
if (!Number.isFinite(_amount)) errors.detail("amount", "Amount required and must be a number, got: {input}.", { input: amount || null });
if (!Number.isInteger(_nMonths)) errors.detail("nMonths", "Number of months required and must be a integer, got: {input}.", { input: nMonths || null });
if (!Number.isFinite(_interestRate)) errors.detail("interestRate", "Interest rate required and must be a number, got: {input}.", { input: interestRate || null });
if (_precision && !Number.isInteger(_precision)) errors.detail("precision", "Precision must be an integer, got: {input}.", { input: precision || null });
if (errors.count()) throw errors.reason("calculator.mortgage.validation", "Parameters validation failed").create();
let monthlyPayment = await this.mortgageService.calculate(_amount, _nMonths, _interestRate, _precision);
return {
monthlyPayment,
total: monthlyPayment * _nMonths,
totalInterest: (monthlyPayment * _nMonths) - _amount
};
}
@Public()
@Post("/missing")
public async missing(@Body() req: any): Promise<number> {
return this.missingService.calculate(req);
}
@Public()
@Post("/unhandled")
public async unhandled(@Body() req: any): Promise<number> {
return this.unhandledService.calculate(req);
}
}
services/mortgage.ts
simple mortgage monthly payment calculator service,BadRequest.builder()
returns a instance ofApiErrorBuilder
allowing to progressively compose validation errors; in this case inputs are expected to be positive numbers.
@Service(MortgageApi)
export class MortgageService implements MortgageApi {
@Remote()
public async calculate(amount: number, nMonths: number, interestRate: number, precision: number = 5): Promise<number> {
// Range validation
let errors: ApiErrorBuilder = BadRequest.builder();
if (amount <= 0) errors.detail("amount", "Amount must be grater than zero." );
if (nMonths <= 0) errors.detail("nMonths", "Number of months must be grater than zero.");
if (interestRate <= 0) errors.detail("interestRate", "Interest rate must be grater than zero.");
if (errors.count()) throw errors.reason("mortgage.calculate.validation", "Invalid parameters values").create();
interestRate = interestRate / 100 / 12;
let x = Math.pow(1 + interestRate, nMonths);
return +((amount * x * interestRate) / (x - 1)).toFixed(precision);
}
}
Proxy implementation
proxies/mortgage.ts
Mortgage calculator is deployed as a dedicated Lambda function, to demonstrate that errors just as the return value is transparently passed.
@Proxy(MortgageApi)
export class MortgageProxy extends LambdaProxy implements MortgageApi {
public calculate(amount: any, nMonths: any, interestRate: any): Promise<number> {
return this.proxy(this.calculate, arguments);
}
}
proxies/missing.ts
Calling the proxy results in error due to non-existence of the target function
@Proxy(MissingApi)
export class MissingProxy extends LambdaProxy implements MissingApi {
public calculate(req: any): Promise<number> {
return this.proxy(this.calculate, arguments);
}
}
proxies/unhandled.ts
Calling the proxy results in unhandled error
@Proxy(UnhandledApi)
export class UnhandledProxy extends LambdaProxy implements UnhandledApi {
public calculate(req: any): Promise<number> {
return this.proxy(this.calculate, arguments);
}
}
Lambda function
functions/calculator.ts
let container = new LambdaContainer("tyx-sample7")
.register(MortgageProxy)
.publish(CalculatorService);
export const handler: LambdaHandler = container.export();
functions/mortgage.ts
let container = new LambdaContainer("tyx-sample7")
.publish(MortgageService);
export const handler: LambdaHandler = container.export();
functions/unhandled.ts
Unhandled error is thrown instead usingcallback(err, null)
for handled errors
export function handler(event: any, ctx: any, callback: (err, data) => void) {
throw new Error("Not Implemented");
}
Serverless file
service: tyx-sample7
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
INTERNAL_SECRET: 7B2A62EF85274FA0AA97A1A33E09C95F
INTERNAL_TIMEOUT: 5s
LOG_LEVEL: DEBUG
# permissions for all functions
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "arn:aws:lambda:${opt:region, self:provider.region}:*:*"
functions:
mortgage-function:
handler: functions/mortgage.handler
unhandled-function:
handler: functions/unhandled.handler
calculator-function:
handler: functions/calculator.handler
events:
- http:
path: mortgage
method: POST
cors: true
- http:
path: missing
method: POST
cors: true
- http:
path: unhandled
method: POST
cors: true
Sample responses
When posting to /demo/mortgage
a valid request:
{
"amount": "15000",
"nMonths": "15",
"interestRate": "7",
"precision": "2"
}
response is received:
{
"monthlyPayment": 1047.3,
"total": 15709.5,
"totalInterest": 709.5
}
When any of required inputs is missing or not a number:
{
"amount": "15000",
"interestRate": "zero",
"precision": "2"
}
HTTP 404 Bad Request is received with the error as json body:
{
"code": 400,
"message": "Parameters validation failed",
"reason": {
"code": "calculator.mortgage.validation",
"message": "Parameters validation failed"
},
"details": [{
"code": "nMonths",
"message": "Number of months required and must be a integer, got: null.",
"params": {
"input": null
}
}, {
"code": "interestRate",
"message": "Interest rate required and must be a number, got: zero.",
"params": {
"input": "zero"
}
}],
"stack": "BadRequest: Parameters validation failed\n at CalculatorService.<anonymous> (/var/task/services/calculator.js:44:103)\n at next (native)\n at /var/task/services/calculator.js:19:71\n at __awaiter (/var/task/services/calculator.js:15:12)\n at CalculatorService.mortgage (/var/task/services/calculator.js:28:16)\n at /var/task/node_modules/tyx/core/container/instance.js:202:47\n at next (native)",
"__class__": "BadRequest"
}
Sending a negative value will return an error generated in MortgageService
:
{
"amount": "15000",
"nMonths": "15",
"interestRate": "-7",
"precision": "2"
}
Error repsonse:
{
"code": 400,
"message": "Invalid parameters values",
"proxy": true,
"reason": {
"code": "mortgage.calculate.validation",
"message": "Invalid parameters values"
},
"details": [{
"code": "interestRate",
"message": "Interest rate must be grater than zero."
}],
"stack": "BadRequest: Invalid parameters values\n at MortgageService.<anonymous> (/var/task/services/mortgage.js:34:99)\n at next (native)\n at /var/task/services/mortgage.js:16:71\n at __awaiter (/var/task/services/mortgage.js:12:12)\n at MortgageService.calculate (/var/task/services/mortgage.js:24:16)\n at /var/task/node_modules/tyx/core/container/instance.js:173:47\n at next (native)",
"__class__": "BadRequest"
}
On purpose there is no check on valid range for precision to demonstrate handling of runtime errors:
{
"amount": "15000",
"nMonths": "15",
"interestRate": "7",
"precision": "25"
}
Responds with 500 Internal Server Error:
{
"code": 500,
"message": "toFixed() digits argument must be between 0 and 20",
"proxy": true,
"cause": {
"stack": "RangeError: toFixed() digits argument must be between 0 and 20\n at Number.toFixed (native)\n at MortgageService.<anonymous> (/var/task/services/mortgage.js:37:61)\n at next (native)\n at /var/task/services/mortgage.js:16:71\n at __awaiter (/var/task/services/mortgage.js:12:12)\n at MortgageService.calculate (/var/task/services/mortgage.js:24:16)\n at /var/task/node_modules/tyx/core/container/instance.js:173:47\n at next (native)\n at /var/task/node_modules/tyx/core/container/instance.js:7:71\n at __awaiter (/var/task/node_modules/tyx/core/container/instance.js:3:12)",
"message": "toFixed() digits argument must be between 0 and 20",
"__class__": "RangeError"
},
"stack": "InternalServerError: toFixed() digits argument must be between 0 and 20\n at LambdaContainer.<anonymous> (/var/task/node_modules/tyx/aws/container.js:49:56)\n at throw (native)\n at rejected (/var/task/node_modules/tyx/aws/container.js:5:65)",
"__class__": "InternalServerError"
}
Posting to /demo/missing
with any json body will result in:
{
"code": 500,
"message": "Function not found: arn:aws:lambda:us-east-1:9999999999:function:tyx-sample7-demo-missing-function",
"cause": {
"stack": "ResourceNotFoundException: Function not found: arn:aws:lambda:us-east-1:9999999999:function:tyx-sample7-demo-missing-function\n at Object.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/json.js:48:27)\n at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/rest_json.js:45:8)\n at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:105:20)\n at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10)\n at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14)\n at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)\n at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)\n at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10\n at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)\n at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12)",
"message": "Function not found: arn:aws:lambda:us-east-1:9999999999:function:tyx-sample7-demo-missing-function",
"code": "ResourceNotFoundException",
"name": "ResourceNotFoundException",
"time": "2017-10-31T08:48:27.688Z",
"requestId": "435de987-be18-11e7-b667-657e489d6573",
"statusCode": 404,
"retryable": false,
"retryDelay": 75.5332334968972,
"__class__": "Error"
},
"stack": "InternalServerError: Function not found: arn:aws:lambda:us-east-1:9999999999:function:tyx-sample7-demo-missing-function\n at MissingProxy.<anonymous> (/var/task/node_modules/tyx/aws/proxy.js:45:52)\n at throw (native)\n at rejected (/var/task/node_modules/tyx/aws/proxy.js:5:65)\n at process._tickDomainCallback (internal/process/next_tick.js:135:7)",
"__class__": "InternalServerError"
}
Posting to /demo/unhandled
with any json body will result in:
{
"code": 500,
"message": "RequestId: 726809f7-be19-11e7-bdb3-c7331d9214c8 Process exited before completing request",
"stack": "InternalServerError: RequestId: 726809f7-be19-11e7-bdb3-c7331d9214c8 Process exited before completing request\n at UnhandledProxy.<anonymous> (/var/task/node_modules/tyx/aws/proxy.js:52:52)\n at next (native)\n at fulfilled (/var/task/node_modules/tyx/aws/proxy.js:4:58)\n at process._tickDomainCallback (internal/process/next_tick.js:135:7)",
"__class__": "InternalServerError"
}
2.8. Configuration
TyX containers require the presence of the Configuration service, if one is not provided a default implementation is being used. In this example the Configuration service is extended with properties relevant to the simple timestamp service.
API definition
Recommended convention is to name extension as ConfigApi and ConfigService. The API must extend the Configuration
interface, so it merged constant (service name) equals Configuration
as well.
api/config.ts
extended configuration
export const ConfigApi = Configuration;
export interface ConfigApi extends Configuration {
timestampSecret: string;
timestampStrength: number;
}
api/timestamp.ts
example timestamp service
export interface TimestampApi {
issue(data: any): TimestampResult;
verify(input: TimestampResult): TimestampResult;
}
export interface TimestampResult {
id: string;
timestamp: string;
hash: string;
signature: string;
data: any;
valid?: boolean;
error?: string;
}
Config service implementation
The implementation extends the provided BaseConfiguration
class that is simple wrapper around a json object this.config
which is process.env
by default. This approach uses Environment Variables of Lambda functions that are also supported by the Serverless Framework, so configurations can be modified via AWS Console or API without a need to redeploy the function. Developers can directly implement the Configuration
interface to use different storage.
@Service(ConfigApi)
export class ConfigService extends BaseConfiguration implements ConfigApi {
constructor(config?: any) {
super(config);
}
get timestampSecret() { return this.config.TIMESTAMP_SECRET; }
get timestampStrength() { return parseInt(this.config.TIMESTAMP_STRENGTH || 0); }
}
Timestamp service implementation
Example timestamp service based on SHA256.
@Service(TimestampApi)
export class TimestampService extends BaseService implements TimestampApi {
@Inject(ConfigApi)
protected config: ConfigApi;
@Public()
@Post("/issue")
public issue( @Body() data: any): TimestampResult {
let result = { id: UUID(), timestamp: new Date().toISOString(), hash: null, signature: null, data };
let text = JSON.stringify(data);
[result.hash, result.signature] = this.sign(result.id, result.timestamp, text);
return result;
}
@Public()
@Post("/verify")
public verify( @Body() input: TimestampResult): TimestampResult {
if (!input.id || !input.timestamp || !input.hash || !input.signature || !input.data)
throw new BadRequest("Invalid input format");
let hash: string, signature: string;
[hash, signature] = this.sign(input.id, input.timestamp, JSON.stringify(input.data));
if (hash !== input.hash) input.error = "Hash mismatch";
else if (signature !== input.signature) input.error = "Invalid signature";
else input.valid = true;
return input;
}
private sign(id: string, timestamp: string, input: string): [string, string] {
if (!this.config.timestampSecret) throw new InternalServerError("Signature secret not configured");
if (!this.config.timestampStrength) throw new InternalServerError("Signature strength not configured");
let hash: string = SHA256(input || "");
let signature: string = id + "/" + timestamp + "/" + hash;
for (let i = 0; i < this.config.timestampStrength; i++)
signature = SHA256(signature + "/" + i + "/" + this.config.timestampSecret);
return [hash, signature];
}
}
Lambda function
functions/timestamp.ts
let container = new LambdaContainer("tyx-sample8")
.register(ConfigService)
.publish(TimestampService);
export const handler: LambdaHandler = container.export();
Serverless file
The two configuration parameters are defined on function level where they are used. There is a limitation that "total size of the set does not exceed 4 KB" per function so better define variables under provider
only when used by all or significant number of functions.
service: tyx-sample8
provider:
name: aws
region: us-east-1
stage: demo
runtime: nodejs6.10
memorySize: 128
timeout: 10
environment:
STAGE: ${self:service}-${opt:stage, self:provider.stage}
LOG_LEVEL: DEBUG
functions:
timestamp-function:
handler: functions/timestamp.handler
environment:
TIMESTAMP_SECRET: F72001057DDA40D3B7B81E7BF06CF495
TIMESTAMP_STRENGTH: 3
events:
- http:
path: issue
method: POST
cors: true
- http:
path: verify
method: POST
cors: true
Sample responses
When posting to /demo/issue
a json object:
{
"from": "tyx",
"to": "world",
"message": "Hello World ..."
}
signed timestamp is received:
{
"id": "c43c1de9-9561-47d3-8aed-10e4e7080b59",
"timestamp": "2017-10-31T10:48:03.903Z",
"hash": "760c891dd1061a843bf9a778e2fb42d28ea6aa57654474cd176ee5385c674875",
"signature": "a3d71713bca7830d9f8b10f7841758db0e7bfd0bfcb2a450fd0caa3d8a72eca2",
"data": {
"from": "tyx",
"to": "world",
"message": "Hello World ..."
}
}
3. Concepts Overview
TyX Core Framework aims to provide a programming model for back-end serverless solutions by leveraging TypeScript support for object oriented programming. TyX addresses how to write and structure the application back-end into services deployed as Lambda functions.
Decorators are extensively used while inheritance from base classes is minimized. Services so written are abstracted from details how HTTP events arrive, how the response is propagated back and the internal routing in case of multiple events being served by the hosting Lambda function. These responsibilities are handled by a Container specific to the hosting environment (Lambda). As proof-of-concept and to serve as development tool an Express based container is provided allowing to run the unmodified services code.
TyX was developed with intent to be used together with Serverless Framework that provides rapid deployment. There is no direct dependency on the Serverless Framework so developers can opt for alternative deployment tools.
3.1. Serverless Environment
AWS Lambda and API Gateway are the core component of AWS Serverless Platform. API Gateway takes most of the responsibilities traditionally handled by HTTP Web Servers (e.g. Apache, nginx, IIS ...), it is the entry point for HTTP requests; however it does not directly host or manage code responsible for handling those requests. AWS Lambda is a compute service for running code without provisioning or managing servers. Lambda functions react on events, API Gateway being one of the supported sources. On each HTTP request arriving on API Gateway an event object is dispatched to an instance of a Lambda function, on its completion the function provides the response (statusCode, headers, body).
Lambda functions are subject to limitation on memory and allowed execution time, and are inherently stateless. AWS Lambda may create, destroy or reuse instances of the Lambda function to accommodate the demand (traffic). The function instance is not aware of its life-cycle. At most it can detect when handler function is first loaded but there is no notification/events when the instance is to be frozen for reuse or about to be destroyed. This prevents any meaningful internal state or cache as the function is not a continuously running process. Limited access to the file system is allowed but should not be used with assumption that stored files will be available for the next request; certainly not to run a local database.
Number of concurrently running function instances is also limited (per AWS account). Developers have no control over the max number of instances a given function can have, which is a challenge when other services or resources used by the function (e.g. database) can not support or scale to match the concurrent requests. Serverless concept removes the need to manage servers or containers for the function (business logic) execution but this does not cover the management of services those functions use. For example S3 most likely can accommodate any load of object access and manipulation concurrent Lambda functions can generate; on the contrary databases usually have limits on concurrent connections and/or the allowed read/write throughput. The Serverless environment may look very restricted and even hostile toward some traditional practices (like the mentioned in-memory caching, or local disk databases). Lambda functions are not intended to serve static files or render HTML content as with MVC frameworks, handling file uploads is also best avoided.
TyX framework does not shield the developer from the specifics and challenges of the serverless architecture, nor does it attempt to abstract the limitations of the execution environment.
3.2. Service
Services are the building block of application back-end and together with dependency injection provides for structured and flexible code organization. A service can expose only a single method with event binding, and if hosted in a dedicated Lambda function will abide to the single responsibility principle. However it may group together methods corresponding to related actions or entities, following microservice principles. TyX does not enforce a specific style or paradigm; a Lambda function can host arbitrary number of services each with its own event bindings.
Traditionally web frameworks especially those based on MVC pattern make a distinction betwe