Rise aims to reduce the amount of boilerplate and repeated code developers often end up writing when building serverless apis. This document shows a before and after example of how much code rise api can help you remove.
Here is an example of a valid rise api app which takes a more traditional serverless approach, one endpoint handled by one function:
const AWS = require('aws-sdk')
/**
* Helper DB function
* Check if member is part of group
*/
async function getGroupMember(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.get({
TableName: process.env.TABLE,
Key: {
pk: input.pk,
sk: input.sk
}
})
.promise()
return item.Item || false
}
/**
* Helper DB function
* Add Note
*/
async function makeGroupNote(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.put({
TableName: process.env.TABLE,
Item: input
})
.promise()
return item.Item || false
}
module.exports = {
api: {
makeNote: (input) => {
/**
* Validation
*/
if (input.group) {
return {
status: 400,
message: 'input must have a group property'
}
}
if (input.content) {
return {
status: 400,
message: 'input must have a content property'
}
}
if (input.authorizationToken) {
return {
status: 400,
message: 'input must have an authorizationToken'
}
}
/**
* Parse JWT
* This function would be too big to write out in this
* example
*/
const userId = parseJwt(input.authorizationToken)
/**
* Guard against calls whose user is not part of
* group
*/
const user = await getGroupMember({
pk: input.group,
sk: userId
})
if (!user) {
return {
status: 400,
message: 'Unauthorized'
}
}
/**
* Make note id
*/
const id = 'note_' + AWS.util.uuid.v4()
/**
* Make Group Note
*/
await makeGroupNote({
pk: input.group,
sk: id,
content: input.content
})
return {
id,
group: input.group,
content: input.content
}
}
},
config: {
name: 'example
}
}
All of the steps in this function are very common. RiseApi aims to help streamline these steps by allowing you to defining actions in an array.
First lets swap out the validation with the input validation step. This means we need to take the existing function, put into an array, and insert the input validation step before it:
const AWS = require('aws-sdk')
/**
* Helper DB function
* Check if member is part of group
*/
async function getGroupMember(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.get({
TableName: process.env.TABLE,
Key: {
pk: input.pk,
sk: input.sk
}
})
.promise()
return item.Item || false
}
/**
* Helper DB function
* Add Note
*/
async function makeGroupNote(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.put({
TableName: process.env.TABLE,
Item: input
})
.promise()
return item.Item || false
}
module.exports = {
api: {
makeNote: [
{
type: 'input',
authorizationToken: 'string',
group: 'string',
content: 'string'
},
(input) => {
/**
* Parse JWT
* This function would be too big to write out in this
* example
*/
const userId = parseJwt(input.authorizatinToken)
/**
* Guard against calls whose user is not part of
* group
*/
const user = await getGroupMember({
pk: input.group,
sk: userId
})
if (!user) {
return {
status: 400,
message: 'Unauthorized'
}
}
/**
* Make note id
*/
const id = 'note_' + AWS.util.uuid.v4()
/**
* Make Group Note
*/
await makeGroupNote({
pk: input.group,
sk: id,
content: input.content
})
return {
id,
group: input.group,
content: input.content
}
}
]
},
config: {
name: 'example
}
}
Next, lets replace our db call to check if the user is in the group. This will mean 2 things:
!id
) rather than parsing the jwt token manually. There are many keywords in rise api, when any keyword starts with !
, it means we are getting a property off of the cognito user, parsed from the jwt. So !id
means userObjectFromParsedJwt.id
const AWS = require('aws-sdk')
/**
* Helper DB function
* Add Note
*/
async function makeGroupNote(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.put({
TableName: process.env.TABLE,
Item: input
})
.promise()
return item.Item || false
}
module.exports = {
api: {
makeNote: [
{
type: 'input',
group: 'string',
content: 'string'
},
{
type: 'guard',
pk: '$group',
sk: '!id'
},
(input) => {
/**
* Make note id
*/
const id = 'note_' + AWS.util.uuid.v4()
/**
* Make Group Note
*/
await makeGroupNote({
pk: input.group,
sk: id,
content: input.content
})
return {
id,
group: input.group,
content: input.content
}
}
]
},
config: {
name: 'example
}
}
Next, we will replace our code to make a note id with the "add" step. This add step adds data to the actions state. We will also use a special keyword @id
to generate a random id.
const AWS = require('aws-sdk')
/**
* Helper DB function
* Add Note
*/
async function makeGroupNote(input) {
const region = process.env.AWS_REGION || 'us-east-1'
const db = new AWS.DynamoDB.DocumentClient({
region: region
})
const item = await db
.put({
TableName: process.env.TABLE,
Item: input
})
.promise()
return item.Item || false
}
module.exports = {
api: {
makeNote: [
{
type: 'input',
group: 'string',
content: 'string'
},
{
type: 'guard',
pk: '$group',
sk: '!id'
},
{
type: 'add',
id: 'note_{@id}'
}
(input) => {
/**
* Make Group Note
*/
await makeGroupNote({
pk: input.group,
sk: input.id,
content: input.content
})
return {
id,
group: input.group,
content: input.content
}
}
]
},
config: {
name: 'example
}
}
Next, we will replace that code that adds the note into the database with a db set
step
module.exports = {
api: {
makeNote: [
{
type: 'input',
group: 'string',
content: 'string'
},
{
type: 'guard',
pk: '$group',
sk: '!id'
},
{
type: 'add',
id: 'note_{@id}'
},
{
type: 'db',
action: 'set',
input: {
pk: '$group',
sk: '$id',
content: '$content'
}
},
(input) => {
return {
id,
group: input.group,
content: input.content
}
}
]
},
config: {
name: 'example
}
}
Finally, we will define what values we will return by defining which properties on the state we will return in an output step
module.exports = {
api: {
makeNote: [
{
type: 'input',
group: 'string',
content: 'string'
},
{
type: 'guard',
pk: '$group',
sk: '!id'
},
{
type: 'add',
id: 'note_{@id}'
},
{
type: 'db',
action: 'set',
input: {
pk: '$group',
sk: '$id',
content: '$content'
}
},
{
type: 'output',
group: 'string',
id: 'string',
content: 'string'
}
]
},
config: {
name: 'example
}
}
We have reduced many lines of commonly repeated code into an array of step definitions. The great thing here is that it does not need to be a choice between custom functions or risepi steps. You can mix and match them however you like. For example:
module.exports = {
api: {
makeNote: [
{
type: 'input',
group: 'string',
content: 'string'
},
{
type: 'guard',
pk: '$group',
sk: '!id'
},
(input) => {
if (input.content.includes('this group sucks')) {
throw new Error('Need to make a more helpful criticism')
}
return input
},
{
type: 'add',
id: 'note_{@id}'
},
{
type: 'db',
action: 'set',
input: {
pk: '$group',
sk: '$id',
content: '$content'
}
},
async (input) => {
const result = await alsoWriteToSQLDatabase(input)
return input
},
{
type: 'output',
group: 'string',
id: 'string',
content: 'string'
}
]
},
config: {
name: 'example
}
}