The functionly library
lets you build serverless
nodejs applications in an innovative, functional, and fun by abstraction way.
Use the JavaScript language and the JSON syntax to describe infrastructure and entities, service dependencies, and to implement service code. Deploy your solution to cloud providers, or run containerized on your onprem servers, or locally during development time using the functionly CLI
.
Defining a rest service which listens on /hello-world
:
import { FunctionalService, rest, description, param } from 'functionly'
@rest({ path: '/hello-world' })
@description('hello world service')
export class HelloWorld extends FunctionalService {
static async handle(@param name = 'world') {
return `hello ${name}`
}
}
export const helloworld = HelloWorld.createInvoker()
Running on localhost:
functionly start
Try it on http://localhost:3000/hello-world?name=Joe
npm install functionly -g
npm install functionly
- Create an empty Functionly project
- Create a Hello world service
- Create a Todo application with DynamoDB
- Run and Deploy with CLI
- AWS deployment
- Examples
It's a simple npm project.
- functionly
npm install --save functionly
Functionly uses webpack with babel for compile the code.
- babel-core
- babel-loader
- babel-preset-functionly-aws
npm install --save-dev babel-core babel-loader babel-preset-functionly-aws
Default .babelrc
{
"presets": [ "functionly-aws" ]
}
Default functionly.json
{
"awsRegion": "us-east-1",
"main": "./src/index.js",
"deployTarget": "aws",
"localPort": 3000,
"stage": "dev",
"watch": true,
"compile": "babel-loader"
}
We need to create a FunctionalService
to implement the business logic of hello world application
import { FunctionalService } from 'functionly'
export class HelloWorld extends FunctionalService {
static async handle() {}
}
If you want your service to be accessible with a web request over a rest interface then you have to decorate it with the rest decorator. We have to set the path
property to define the rest endpoint.
If we do not set the methods
property that means it will accept GET
requests. (default: methods: ['get']
)
@rest({ path: '/hello-world' })
Define a description for the HelloWorld
, which will make it easier to find in the AWS Lambda list.
@description('hello world service')
Now we have to create the business logic.
import { FunctionalService, rest, description } from 'functionly'
@rest({ path: '/hello-world' })
@description('hello world service')
export class HelloWorld extends FunctionalService {
static async handle() {
return `hello world`
}
}
We are almost done, we just have to export our service from the main file.
export const helloworld = HelloWorld.createInvoker()
In the handle
method if you use the @param
property decorator for a parameter then it resolves the value from a request context.
import { FunctionalService, rest, description, param } from 'functionly'
@rest({ path: '/hello-world' })
@description('hello world service')
export class HelloWorld extends FunctionalService {
static async handle(@param name = 'world') {
return `hello ${name}`
}
}
export const helloworld = HelloWorld.createInvoker()
Define a base class for FunctionalService to set basic Lambda settings in the AWS environment.
import { FunctionalService, aws } from 'functionly'
@aws({ type: 'nodejs20.x', memorySize: 512, timeout: 3 })
export class TodoService extends FunctionalService { }
We need a DynamoTable, called TodoTable
because we want to store todo items.
import { DynamoTable, dynamoTable, injectable } from 'functionly'
@injectable()
@dynamo()
export class TodoTable extends DynamoTable { }
We need to create a service to read todo items.
export class GetAllTodos extends TodoService {
static async handle() {}
}
If you want your service to be accessible with a web request over a rest interface then you have to decorate it with the rest decorator. We have to set the path
property to define the rest endpoint.
If we do not set the methods
property that means it will accept GET
requests. (default: methods: ['get']
)
@rest({ path: '/getAllTodos' })
Define a description for the TodoService
, which will make it easier to find in the AWS Lambda list.
@description('get all Todo service')
Now we have to create the business logic. We want to read the todo items, so we need to inject the TodoTable
. Get the items from it and return from our service.
import { rest, description, inject } from 'functionly'
@rest({ path: '/getAllTodos' })
@description('get all Todo service')
export class GetAllTodos extends TodoService {
static async handle(@inject(TodoTable) db) {
let items = await db.scan()
return { ok: 1, items }
}
}
We are almost done, we just have to export our service from the main file.
export const getAllTodos = GetAllTodos.createInvoker()
We need a service to create todo items, so let's do this. We will also define a rest endpoint and a description.
import { rest, description } from 'functionly'
@rest({ path: '/createTodo', methods: ['post'] })
@description('create Todo service')
export class CreateTodo extends TodoService {
static async handle() {}
}
We need some values to create a new todo item: name
, description
and status
. Expect these with the param decorator, and it will resolve them from the invocation context.
import { rest, description, param } from 'functionly'
@rest({ path: '/createTodo', methods: ['post'] })
@description('create Todo service')
export class CreateTodo extends TodoService {
static async handle(@param name, @param description, @param staus) {}
}
The business logic: save a new todo item. Inject the TodoTable
and save a new todo item with the put
function. We need an id for the new todo, in the example, we'll use shortid to generate them.
import { generate } from 'shortid'
import { rest, description, param } from 'functionly'
@rest({ path: '/createTodo', methods: ['post'] })
@description('create Todo service')
export class CreateTodo extends TodoService {
static async handle(@param name, @param description, @param status, @inject(TodoTable) db) {
let item = {
id: generate(),
name,
description,
status
}
await db.put({ Item: item })
return { ok: 1, item }
}
}
export const createTodo = CreateTodo.createInvoker()
Optional
Create two services: validate and persist todo items. Then the CreateTodo has only to call these services.
It will be an injectable service and expect the three todo values, then implement a validation logic in the service.
import { injectable, param } from 'functionly'
@injectable()
export class ValidateTodo extends Service {
static async handle( @param name, @param description, @param status) {
const isValid = true
return { isValid }
}
}
It will be an injectable service and expect the three todo values and inject a TodoTable
then implement a persist logic in the service.
import { injectable, param, inject } from 'functionly'
@injectable()
export class PersistTodo extends Service {
static async handle( @param name, @param description, @param status, @inject(TodoTable) db) {
let item = {
id: generate(),
name,
description,
status
}
await db.put({ Item: item })
return item
}
}
inject the two new services(ValidateTodo
, PersistTodo
) and change the business logic
import { rest, description, param, inject } from 'functionly'
@rest({ path: '/createTodo', methods: ['post'] })
@description('create Todo service')
export class CreateTodo extends TodoService {
static async handle(
@param name,
@param description,
@param status,
@inject(ValidateTodo) validateTodo,
@inject(PersistTodo) persistTodo
) {
let validateResult = await validateTodo({ name, description, status })
if (!validateResult.isValid) {
throw new Error('Todo validation error')
}
let persistTodoResult = await persistTodo({ name, description, status })
return { ok: 1, persistTodoResult }
}
}
The source code of this example is available here
npm install
The CLI helps you to deploy and run the application.
- CLI install
npm install functionly -g
- Create DynamoDB with Docker
docker run -d --name dynamodb -p 8000:8000 peopleperhour/dynamodb
- Deploy will create the tables in DynamoDB
Note: Create the functionly.json in the project for short commands. Also, you don't have to pass all arguments.
functionly deploy local
During development, you can run the application on your local machine.
functionly start
Disclaimer: As functionly provisions AWS services, charges may apply to your AWS account. We suggest you to visit https://aws.amazon.com/pricing/services/ to revise the possible AWS costs.
Set up AWS Credentials before deployment.
Note: Create the functionly.json in the project for short commands. Also, you don't have to pass all arguments. As the
deployTarget
is configured asaws
(the default value configured) then the deploy command will use this as deployment target.
Functionly will create the package and deploy the application to AWS. The package is a CloudFormation template, it contains all the AWS resources so AWS can create or update the application's resources based on the template.
functionly deploy
Congratulations! You have just created and deployed your first
functionly
application!