How to Build a Serverless Web Application
Tags: aws, cloud, serverless, webdev
Published: 2024-03-12 18:45:31

Ah, serverless computing - the buzzword that's been taking the tech world by storm. But what exactly is it, and why should you care? Simply put, serverless computing allows you to build and run applications without worrying about provisioning or managing servers. It's like having an army of invisible servers at your beck and call, ready to handle your application's logic whenever it's needed.

One of the coolest use cases for serverless technology is building web applications. Imagine a world where you can create a website without having to worry about scaling, patching, or maintaining servers. Sounds too good to be true, right? Well, buckle up, because that's exactly what we're going to do in this article.

Serverless is a cloud computing execution model where the cloud provider dynamically manages the allocation and provisioning of servers. With serverless, you don't have to worry about provisioning, scaling, or managing servers yourself. Instead, you deploy your code, and the cloud provider handles the server management and execution for you.

In a serverless architecture, your application's code runs in stateless compute containers that are event-triggered, ephemeral (they only run when needed and shut down automatically after use), and fully managed by the cloud provider. These containers, called "functions" in the serverless world, are typically triggered by events such as HTTP requests, database events, queue services, monitoring alerts, or other custom triggers.

The key characteristics of serverless computing include:

1. **No Server Management**: You don't have to provision, scale, or manage any servers. The cloud provider handles all the infrastructure management for you.

2. **Event-Driven**: Functions are triggered by specific events or requests, rather than running continuously.

3. **Automatic Scaling**: The cloud provider automatically allocates the necessary resources to handle the incoming workload, scaling out seamlessly as needed.

4. **Pay-per-Use Billing**: You only pay for the compute time your functions consume when they are running, plus any other cloud services used. This can result in significant cost savings compared to maintaining always-on servers.

5. **High Availability and Fault Tolerance**: Cloud providers ensure high availability and fault tolerance for serverless functions across multiple availability zones.

Popular serverless compute services include AWS Lambda, Google Cloud Functions, Azure Functions, and IBM Cloud Functions. These services allow you to deploy and run your code without provisioning or managing servers, enabling you to focus on writing application logic instead of infrastructure management.

Serverless architectures are well-suited for various workloads, including web applications, APIs, data processing, IoT backends, and more. They can help reduce operational overhead, increase scalability, and potentially lower costs for certain types of applications.

**Setting the Stage**

Before we dive into the code, let's set up our development environment. You'll need an AWS account and the AWS CLI installed on your machine. We'll also be using Node.js and the Serverless Framework, a popular open-source tool that makes building and deploying serverless applications a breeze.

First, install the Serverless Framework globally:

```bash
npm install -g serverless
```

Next, create a new directory for your project and navigate to it:

```bash
mkdir serverless-web-app
cd serverless-web-app
```

Initialize a new Serverless project:

```bash
serverless create --template aws-nodejs --path my-service
```

This will create a new directory `my-service` with a basic Lambda function and other boilerplate files.

**Building the Web Application**

Our serverless web application will consist of two main components: a static website hosted on AWS S3, and a Lambda function to handle server-side logic. Let's start with the static website.

Create a new directory called `website` in your project's root directory, and add an `index.html` file with some basic HTML:

```html
<!-- website/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Serverless Web App</title>
  </head>
  <body>
    <h1>Welcome to my Serverless Web App!</h1>
    <p>This is a static website hosted on AWS S3.</p>
  </body>
</html>
```

Now, let's configure our Serverless project to deploy this website to an S3 bucket. Open `serverless.yml` and add the following resources:

```yaml
# serverless.yml
service: serverless-web-app

provider:
  name: aws
  runtime: nodejs18.x

resources:
  Resources:
    WebsiteBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-${opt:stage, self:provider.stage}
        WebsiteConfiguration:
          IndexDocument: index.html

# ... (existing Lambda function configuration)
```

This will create an S3 bucket with the name `serverless-web-app-[stage]`, where `[stage]` is the deployment stage (e.g., `dev`, `prod`). The bucket will be configured to serve the `index.html` file as the website index.

To deploy the website, run:

```bash
serverless deploy
```

This will create the S3 bucket and upload the `index.html` file. The CLI output will include the website URL, which you can visit in your browser to see the static website.

**Adding Server-Side Logic with Lambda**

Now that we have our static website set up, let's add some server-side logic using AWS Lambda. We'll build a simple "contact form" functionality, where users can submit their name and email, and we'll save the data to a DynamoDB table.

First, we need to create the DynamoDB table. Add the following resource to `serverless.yml`:

```yaml
# serverless.yml
resources:
  Resources:
    # ... (existing resources)
    ContactsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-contacts-${opt:stage, self:provider.stage}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
```

This will create a DynamoDB table called `serverless-web-app-contacts-[stage]` with a hash key `id`.

Next, let's create the Lambda function that will handle the contact form submissions. In the `my-service` directory, create a new file called `handler.js` with the following code:

```javascript
// handler.js
import AWS from 'aws-sdk';

const dynamoDb = new AWS.DynamoDB.DocumentClient();

const TABLE_NAME = process.env.CONTACTS_TABLE;

export const handler = async (event) => {
  const { name, email } = JSON.parse(event.body);

  const params = {
    TableName: TABLE_NAME,
    Item: {
      id: `contact-${Date.now()}`,
      name,
      email,
    },
  };

  try {
    await dynamoDb.put(params).promise();
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Contact form submitted successfully' }),
    };
  } catch (err) {
    console.error(err);
    return {
      statusCode: 500,
      body: JSON.stringify({ message: 'Error submitting contact form' }),
    };
  }
};
```

This Lambda function expects a JSON payload with `name` and `email` properties in the event body. It generates a unique ID for the contact, and stores the contact data in the DynamoDB table.

Finally, we need to configure the Lambda function in `serverless.yml`:

```yaml
# serverless.yml
functions:
  app:
    handler: handler.handler
    events:
      - http:
          path: submit-contact
          method: post
          cors: true
    environment:
      CONTACTS_TABLE: ${self:service}-contacts-${opt:stage, self:provider.stage}
```

This sets up an HTTP endpoint (`/submit-contact`) that will trigger the Lambda function when a POST request is made. The function will have access to the DynamoDB table name through the `CONTACTS_TABLE` environment variable.

To deploy the Lambda function and DynamoDB table, run:

```bash
serverless deploy
```
The complete `severless.yml` file looks like this:

```yaml
service: serverless-web-app

provider:
  name: aws
  runtime: nodejs18.x

resources:
  Resources:
    WebsiteBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-${opt:stage, self:provider.stage}
        WebsiteConfiguration:
          IndexDocument: index.html
    ContactsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-contacts-${opt:stage, self:provider.stage}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST

functions:
  app:
    handler: handler.handler
    events:
      - http:
          path: submit-contact
          method: post
          cors: true
    environment:
      CONTACTS_TABLE: ${self:service}-contacts-${opt:stage, self:provider.stage}
```


**Connecting the Frontend**

With the backend logic in place, let's update the frontend to allow users to submit the contact form. Open `website/index.html` and add a form:

```html
<!-- website/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Serverless Web App</title>
  </head>
  <body>
    <h1>Welcome to my Serverless Web App!</h1>
    <p>This is a static website hosted on AWS S3.</p>

    <h2>Contact Us</h2>
    <form id="contact-form">
      <label for="name">Name:</label>
      <input type="text" id="name" name="name" required />
      <br />
      <label for="email">Email:</label>
      <input type="email" id="email" name="email" required />
      <br />
      <button type="submit">Submit</button>
    </form>

    <script>
      const form = document.getElementById('contact-form');
      form.addEventListener('submit', async (event) => {
        event.preventDefault();

        const name = document.getElementById('name').value;
        const email = document.getElementById('email').value;

        const response = await fetch('/submit-contact', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ name, email }),
        });

        const data = await response.json();
        alert(data.message);
        form.reset();
      });
    </script>
  </body>
</html>
```

This adds a simple contact form to the HTML, and includes a JavaScript script that sends a POST request to the `/submit-contact` endpoint (which is the Lambda function we deployed earlier) when the form is submitted.

To deploy the updated website, run:

```bash
serverless client deploy
```

This will upload the `index.html` file to the S3 bucket again.

Now, if you visit the website URL and submit the contact form, you should see a success message. You can also check the DynamoDB table in the AWS console to see the submitted contact data.

**Wrapping Up**

And there you have it, folks! We've built a serverless web application using AWS Lambda, S3, and DynamoDB. We've covered setting up the development environment, deploying a static website, creating a Lambda function for server-side logic, and connecting the frontend to the backend.

Serverless architectures offer numerous benefits, including scalability, cost-efficiency, and reduced operational overhead. By leveraging services like AWS Lambda, you can focus on writing code and building features, without worrying about server management. It's like having a team of hardworking elves handling the tedious infrastructure tasks for you.

Of course, this is just a simple example, and real-world serverless applications can become much more complex. But hey, we all have to start somewhere, right? The principles and services demonstrated in this article provide a solid foundation for building more advanced serverless web applications.

So, what are you waiting for? Embrace the serverless revolution and start building your next big idea today!