Fenrir is a framework that enriches serverless programming by providing developers with new meta-programming constructs, named annotations. Users mark the serverless functions with annotations that signal the framework which code transformations are going to be needed and which metadata will be used for their deployment.
Fenrir transpiles TypeScript codebases written following AWS Lambda's interfaces,
while the generated metadata is appended to a serverless.yml
file, since
the deployment is entirely delegated to the Serverless framework.
Here is a quick example that converts code written in a monolithic style into a serverless architecture:
// MONOLITH
/**
* $Fixed
* $HttpApi(method: "GET", path: "/orders/report")
*/
export async function processOrder(orderId) {
// ... processing logic ...
console.log(`Processing order ${orderId}`)
return order
}
/** $Scheduled(rate: "2 hours") */
export async function generateReport() {
// get the processed data and generate report
console.log('Generating report')
}
// SERVERLESS CODE
export async function processOrder(event) {
const orderId = event.orderId
// ... processing logic ...
console.log(`Processing order ${orderId}`)
return {
statusCode: 200,
body: JSON.stringify(order),
}
}
// The implementation of `generateReport`
// is omitted as it remains unchanged.
While this is the metadata generated by the previous example:
# SERVERLESS METADATA
processOrder:
handler: output.processOrder
events:
- httpApi:
method: GET
path: /orders/report
generateReport:
handler: output.generateReport
events:
- schedule:
rate: 2 hours
⚠️ Fenrir hasn't been published to NPM yet
Install the CLI:
npm install -g fenrir-cli
Make sure both fenrir-base
and serverless
packages are installed.
The CLI offers a init
command to get things started (it creates a fenrir.config.json
):
fenrir init
However, most of the time the CLI is used with the -g
flag which indicates in which directory the fenrir.config.json
file is located:
fenrir -g functions
Annotations are syntactical units, or keywords, enclosed within JSDoc comments, each associated with their respective transformer. They can be parameterized and composed to form a pipeline of transformations.
It converts monolithic functions into fixed-size serverless functions.
- The monolithic functions’ parameters are mapped to a single event parameter in order to adhere to AWS Lambda serverless functions’ signature.
- The monolithic functions’ return statements change to match the shape of the response expected by the platform, by creating an object with a status code (200) and a body that contains a serialized version of the initially returned value.
- Early return statements and throw statements are modified similarly, but the status code represents a client error (400).
It has no mandatory parameters, however, all the specified arguments will be passed as metadata for the function deployment.
Input:
/** $Fixed(timeout: 10) */
export async function foo(id) {
if (!isValid(id)) {
throw new Error('Something went wrong')
}
const data = await query()
return data
}
Output:
/** $Fixed(timeout: 10) */
export async function foo(event) {
const id = event.id
if (!isValid(id)) {
return {
statusCode: 400,
body: JSON.stringify({
error: 'Something went wrong',
}),
}
}
const data = await query()
return {
statusCode: 200,
body: JSON.stringify(data),
}
}
functions:
foo:
handler: output/source.foo
timeout: 10 # default is 6 seconds
It generates code that monitors and logs the functions’ resource usage by also importing the necessary dependencies, i.e., for AWS Lambda it uses and injects the CloudWatch dependency.
Input:
import { query } from './local'
/**
* $TrackMetrics(namespace: 'shop', metricName: 'sell', metricValue: size)
*/
export async function processOrder(id) {
const order = await query(id)
const size = order.size
// ...more logic...
return size
}
Output:
import { query } from './local'
import { CloudWatch } from 'aws-sdk'
/**
* $TrackMetrics(namespace: 'shop', metricName: 'sell', metricValue: size)
*/
export async function processOrder(id) {
const order = await query(id)
const size = order.size
await new CloudWatch()
.putMetricData({
Namespace: 'shop',
MetricData: [
{
MetricName: 'sell',
Timestamp: new Date(),
Value: size,
},
],
})
.promise()
// ...more logic...
return size
}
It generates the metadata needed to make the function available as an HTTP endpoint.
It generates the metadata needed to make the function run at specific dates or periodic intervals.
Fenrir endorses the creation of new annotations to fit custom requirements and usages.
In order to inform Fenrir of the new annotation name and its transformer
implementation, the configuration file (i.e., fernrir.config.json
) must be updated:
{
"annotations": {
"IoT": "annotations/iot-impl.ts"
}
}
To write a transformer, you can import useful methods from fenrir-base
.
import type { CustomTransformer } from 'fenrir-base'
type IotTransfomer = CustomTransformer<'IoT', { sql: string }>
const transformer: IotTransfomer = (node, context, annotation) => {
// ...implementation...
}
// custom transformers must be exported as `default`
export default transformer
To build all the packages:
turbo build
Contains the transpiler and its transformers.
Run the input code for testing the transformations:
$ turbo run dev --filter core
Bundle into a dist
folder:
$ turbo run build --filter core
Self-explanatory. It relies on the core
package.