Serverless Functions in Depth

Serverless Functions In Depth

A deep-dive on how to build, test, deploy, update, and delete serverless functions with AWS Amplify

In this tutorial, you’ll learn the fundamentals of serverless functions & serverless applications. While this tutorial is focused on front-end developers new to the serverless space, this is a good deep dive for anyone looking to learn more about serverless functions in general.

In a recent article published by UC Berkeley titled “Cloud Programming Simplified: A Berkeley View on Serverless Computing”, this prediction was made:

“Serverless computing will become the default computing paradigm of the Cloud Era, largely replacing serverful computing and thereby bringing closure to the Client-Server Era.”

Serverless has the highest growth of all services in the cloud. The serverless computing industry is projected to grow to $7.7B in 2021, compared to $1.9B in 2016.

Serverless overview

So what is a serverless application? Here’s my definition:

Serverless applications run in stateless compute containers that are event-driven, ephemeral (functions as a service, may last for one invocation), and fully managed by the cloud provider of your choice. These applications scale seamlessly and do not require any server operations.

The idea is that you write your application code in the form of functions. When you deploy your function, you then need a way to invoke it in the form of an event.

The event could come from some type of API gateway (http request) if invoking the code from a client application, an event from another serverless function, or an event from another cloud service (like Amazon S3 for example after something is uploaded).

Your cloud provider executes the function code on your behalf. The cloud provider also takes care of provisioning and managing the servers to run the code upon invocation.

Serverless challenges for front-end developers

The problem for many front-end developers & developers new to cloud-computing is that the user-interface & the process of getting started with this type of architecture is not as straight-forward or easy as it could be & the tooling is out of the realm of what we’re used to working with.

There are many abstractions built on top of cloud providers to make this easier — things like the Serverless Framework, Terraform, & Serverless Application Model (SAM) for example offer frameworks & tools to make provisioning & deploying these resources easier.

Many times though you still need to understand the basic terminology & lingo to get started with these tools.

The Amplify Framework offers a layer of abstraction that I think is ideal for the deployment & provisioning of cloud functions from your front-end environment. I feel this approach is perfect not only for front-end developers but is a leap forward compared to the old ways of doing things in general.

In this tutorial, I’ll walk through the entire process of creating, deploying, updating, connecting to & deleting cloud functions on AWS using the Amplify Framework.

Invoking a function

After deploying a serverless function, the function itself needs to have a way to be invoked. Again, there are three main ways to invoke one of these functions:

  1. Via an HTTP endpoint
  2. Via the aws-sdk (many times from another function)
  3. From an event in the cloud (S3 upon image upload, when an item is created in your database, etc..)

We’ll be invoking the function from our front-end application, so we’ll be interacting with it via an HTTP endpoint. You typically need to do a couple of things to get this to work with AWS:

  1. Create the function.
  2. Create an API endpoint in API Gateway
  3. Configure API Gateway to forward the http request to the function.

The Amplify framework allows us to set all of this up with one command. Let’s get started!

Getting started

The function we’ll be deploying will be a function that fetches Shiba Inu pictures from another API. The Shiba Inu API does not have CORS enabled so we cannot interact with the API directly from our browser. The obvious case for this situation is to build it on the server, so we’ll be doing this in a serverless function.

The first thing we’ll do is create a client application. You can use your framework of choice (i.e. React, Vue, React Native, Angular, etc..) for this project. From this client project, we’ll be making requests to the serverless application we create.

Once you’ve created the client application you will be using to test your functions, change into this directory & move on to the next step.

Installing & configuring the Amplify CLI

The next thing we’ll do is install & configure the Amplify CLI

npm install -g @aws-amplify/cli
amplify configure

If this is your first time configuring Amplify & would like a walkthrough of how this works, check out this video.

Once the CLI has been installed & configured, we’ll initialize a new Amplify project. This will walk us through a few steps & ask questions about how we would like the project to be set up.

amplify init

The CLI should detect the type of project you’re working in & automatically populate most of the fields after you’ve chosen your app name, environment name, & choice of IDE.

The amplify init command is a one-time initialization step for your Amplify powered cloud app. You run this once for each project (JavaScript, iOS, or Android) to connect your app with an AWS backend. To learn more about what this command does behind the scenes, check out the documentation.

Once we’ve initialized our Amplify project, we can create our first Lambda function. To do so, run the following command:

amplify add api
? Please select from one of the below mentioned services REST
? Provide a friendly name for your resource to be used as a label for this category in the project shibaapi
? Provide a path (e.g., /items) /pictures
? Choose a Lambda source ❯ Create a new Lambda function
? Provide a friendly name for your resource to be used as a label for this category in the project: shibafunction
? Provide the AWS Lambda function name: shibafunction
? Choose the function template that you want to use: Serverless express function
?
Do you want to edit the local lambda function now? n
? Restrict API access n
? Do you want to add another path? n

What just happened? The Amplify CLI has provisioned 2 resources: an AWS Lambda function & an API Gateway configuration which allows us to interact with it via an HTTP endpoint. In the folder amplify/backend, we should now see two new folders: api & function.

Now, we can create these resources in our account by running the amplify push command:

amplify push

Now our Lambda function has been deployed & is live!

The Lambda function

The base code for the Lambda function had been created for us by the CLI & is located at amplify/backend/function/shibafunction.

Lambda functions typically have what’s known as a handler function that’s in charge of invoking the Lambda function. You can see our handler function in shibafunction/src/index.js. We could reference & work with this function directly if we’d like to, but in our case we’re using the serverless express framework:

// amplify/backend/function/shibafunction/src/index.js
const awsServerlessExpress = require('aws-serverless-express');
const app = require('./app');
const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
awsServerlessExpress.proxy(server, event, context);
};

The above code will forward all requests to the routes that are defined in shibafunction/src/app.js.

The serverless express framework basically allows us to create an express application complete with routing & http verbs in our Lambda function so we can do more inside of it than with just a basic JavaScript function.

If you look in app.js you’ll see all of the routes that were created for us. Let’s take a look at app.get & app.post:

// amplify/backend/function/shibafunction/src/app.js
app.get('/pictures', function(req, res) {
// Add your code here
res.json({success: 'get call succeed!', url: req.url});
});
app.post('/pictures', function(req, res) {
// Add your code here
res.json({success: 'post call succeed!', url: req.url, body: req.body})
});

In these functions, we have a request & a response (req & res) argument that are available.

The request (req) will have an apiGateway property on all operations (req.apiGateway) that has the event & the context.

  • The context contains all of the metadata about the function itself.
  • The event represents the event or trigger that caused the invocation of the lambda. The event will contain data & metadata about the API call, in our case things like the headers, path, & even user data if the API is protected with authentication.

In a put, post, or delete operation, we’ll also have access to the body object. The body will contain any data that we want to send to the API (we’ll look at how to send the body object in just a moment).

So how do we call this function. We do it with the API category from Amplify. Here’s how it works. API has six different types of operations:

  • API.get
  • API.post
  • API.put
  • API.delete
  • API.head
  • API.graphql

Because we’re working with a REST API, we’ won’t be using the graphql operation.

To invoke an endpoint, you need to set apiName (required), path (required) and options (optional) parameters, and each method returns a Promise:

// definition: API.get(apiName, path, options)
// Use this in your client application to interact with the API
import { API } from 'aws-amplify'
API.get('shibaAPI', '/pictures')
.then(response => console.log({ response }))
.catch(err => console.log({ err }))
// or
const response = await API.get('shibaAPI', '/pictures')
console.log({ response })

If we are using put, post, or delete we can also pass in the body in the third argument:

const body = { userId: '22' }
API.post('shibaAPI', '/pictures', { body }}

If we want to pass headers:

const headers = { username: 'naderdabit' }
API.get('shibaAPI', '/pictures', { headers })

Or we can pass both:

const body = { userId: '22' }
const headers = { username: 'naderdabit' }
API.post('shibaAPI', '/pictures', { body, headers }}

Creating our custom Lambda function

Now, let’s update our function to call the Shiba API.

The first thing we’ll need to do is install axios in our function package so we can make an http request from it. Navigate to amplify/backend/function/shibafunction/src & install it from the command line:

yarn add axios
# or
npm install axios

Next we’ll need to open app.js in our lambda function. Here, we’ll be updating the app.post function to call the API

// amplify/backend/function/shibafunction/src/app.js
// first require axios at the top of the file
const axios = require('axios')
// next, update the app.post function
app.post('/pictures', async function(req, res) {
let number = 5
if (req.body.number) {
number = req.body.number
}

try {
const response = await axios.get(`http://shibe.online/api/shibes?count=${number}`)
const data = response.data
res.json({err: null, success: 'post call succeed!', data })
} catch (err) {
res.json({ err: err })
}
});

Now, since we’ve changed our function code, we need to update it on our backend. That’s pretty easy to do, we can just run the push command again:

amplify push

This will update any changes made in our backend folder to our AWS back end.

Now, we should be able to call the API from our client application. Let’s request 30 pictures from our API:

const shibaData = await API.post('shibaapi', '/pictures', { body: { number: 30 }})
console.log('shibaData:', shibaData)
# or
API.post('shibaapi', '/pictures', { body: { number: 30 }})
.then(data => console.log('data: ', data))
.catch(err => console.log('error:', err))

Testing

Now that we have a function up & running, what if we’d like to make changes & test it locally before deploying it to our environment? Well, we can do that pretty easily. Amplify has a command that allows us to invoke & test a lambda function locally.

Because our function is essentially just an express app, we can start our express server & begin making requests to it via either a curl command or a tool like Postman.

Invoking the function

Let’s invoke a lambda function which in our case will start the express server.

Setting the port
If you’re running a React application, the default port is set as 3000 which is the same default port the express application is going to run on. If you’d like to specify a different port, you can change it in app.js at the bottom of the file:

// shibafunction/src/app.js
app.listen(3001, function() {
console.log("App started")
});

Now, let’s invoke the function:

amplify function invoke shibafunction
  • ? Provide the name of the script file that contains your handler function: index.js
  • ? Provide the name of the handler function to invoke: handler

This will start the express server. We can make curl requests or use something like Postman to make requests:

Get request

curl ‘localhost:3000/pictures’

Post request:

curl ‘localhost:3000/pictures’ -X POST

Post request with body:

curl ‘localhost:3000/pictures’ -X POST 
-d ‘{ “number”: 7 }’ -H “Content-Type: application/json”

Updating the API

What if we’d like to change something about our API, like add additional routes or add authentication. For this, you can run the update command from the CLI:

amplify update api

This will walk you through several steps to update the API. If you would like to add or change routes, you need to first update the configuration with this command before new routes added to your /src/app.js. file will work.

Multiple Environments

What if you’d like to experiment by creating a new function & testing it out without it affecting anything in your current environment?

You can clone your current environment which will deploy entirely new & sandboxed resources that will allow you to test, deploy & delete new features without affecting your main environment.

To learn more about how to set up multiple environments, check out the docs here.

Deleting a function

Now that we have our application up & running, what happens when we’d like to delete our function? We can use the amplify CLI to do this.

Removing the function & api

amplify remove api
amplify remove function
amplify push

Deleting the entire project & all resources

amplify delete

Conclusion

Serverless is one of the most interesting & fastest growing technologies out there today. I’m really excited to be working in this space & I feel like it’s really a good investment of your time to learn this regardless of what platform or cloud provider you build for — Microsoft & Google also offer their own flavor of serverless.

To read about my thoughts about how to leverage serverless to build full-stack applications, check out my post Full-Stack Development in the Era of Serverless Computing.

Amplify also supports many other categories like authentication, GraphQL, S3, & more. To learn more about these categories, check out the documentation here.

My Name is Nader Dabit . I am a Developer Advocate at AWS Mobile working with projects like AWS AppSync and AWS Amplify. I’m also the author of React Native in Action, & the editor of React Native Training & OpenGraphQL.


Serverless Functions in Depth was originally published in A Cloud Guru on Medium, where people are continuing the conversation by highlighting and responding to this story.