Greetings Curious Humans! We’re back with another installment of “Same Service, Different Tools”. Parts 1 to 3 covered SAM, CDK, and Terraform. Today, we’ll explore Architect, a serverless framework focusing on speed and developer agility. Architect is fully open-source and governed by the non-profit OpenJS Foundation. It sets itself apart by curating interfaces for a subset of AWS services that are critical to building web apps. In my opinion, it is the only serverless framework that offers a good local development workflow. We’ll continue using GitHub Codespaces as a base environment that you can fork from my repo, https://github.com/pchinjr/serverless-file-upload

Getting Started

You’ll need to create an admin IAM user and set your /.aws/credentials file:

[arc-admin]
aws_access_key_id=xxx
aws_secret_access_key=xxx

These are the credentials that the Architect CLI will use to deploy your project. One cool thing about Architect is that we’ll be able to build and test everything locally before we deploy!

To bootstrap a project we just need to open a terminal and run npm init @architect serverless-upload-arc. This creates a folder called /serverless-upload-arc that contains our initial project structure. Similar to main.tf or template.yaml, you will find an app.arc file in the root. The .arc file is our declarative manifest. You’ll notice right away that it’s quite small. Resources are declared with pragmas and minimal configuration.

@app
arc-example

@http
get /

@aws
# profile default
region us-west-2

Let’s break it down:

Next, let’s look at the Lambda handler code in /src/http/get-index/index.mjs:

// learn more about HTTP functions here: <https://arc.codes/http>
export async function handler (req) {
  return {
    statusCode: 200,
    headers: {
      'cache-control': 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0',
      'content-type': 'text/html; charset=utf8'
    },
    body: `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Architect</title>
  <style>
     * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } .max-width-320 { max-width: 20rem; } .margin-left-8 { margin-left: 0.5rem; } .margin-bottom-16 { margin-bottom: 1rem; } .margin-bottom-8 { margin-bottom: 0.5rem; } .padding-32 { padding: 2rem; } .color-grey { color: #333; } .color-black-link:hover { color: black; }
  </style>
</head>
<body class="padding-32">
  <div class="max-width-320">
    <img src="<https://assets.arc.codes/logo.svg>" />
    <div class="margin-left-8">
      <div class="margin-bottom-16">
        <h1 class="margin-bottom-16">
          Hello from an Architect Node.js function!
        </h1>
        <p class="margin-bottom-8">
          Get started by editing this file at:
        </p>
        <code>
          arc-example/src/http/get-index/index.mjs
        </code>
      </div>
      <div>
        <p class="margin-bottom-8">
          View documentation at:
        </p>
        <code>
          <a class="color-grey color-black-link" href="<https://arc.codes>"><https://arc.codes></a>
        </code>
      </div>
    </div>
  </div>
</body>
</html>
`

This might look strange at first, but it is a Lambda function triggered by API Gateway that returns an HTML string to render in the browser. Our example service doesn’t have a front-end client, but we can use this default route to try out local development. From the project root /serverless-upload-arc open a terminal and run npx arc sandbox, you should see Architect start a local server and Github Codespaces will forward the ports to a private URL. Ctrl+click on https://localhost:3333 and you should see the index page. We can update the Lambda handler with custom text like “Praise Cage!” in the <h1> element and see the changes in the returned HTML.

Untitled

sandbox is a local server that emulates AWS services like API Gateway and even Dynamodb. It does all this with Nodejs and doesn’t require any extra Java runtimes or Docker containers. We can incrementally build and improve our iteration cycles using the sandbox environment without waiting for a full cloud deployment.

Our first route, SNS topic, and S3 integration

Let’s add a POST /upload route to app.arc, make a new file /src/http/post-upload/index.mjs for our Lambda handler, and use two plugins for creating a public S3 bucket and a local S3 server to emulate our FileUploadBucket. Architect is extendable with plugins that hook into the sandbox lifecycle and can modify the CloudFormation output with custom resources. Under the hood, Architect compiles the .arc file into CloudFormation. Architect takes care of service discovery, IAM roles, code packaging, and service integrations. Check out the Architect playground to see a side-by-side comparison of how much CloudFormation you don’t have to write.

@aws
region us-east-1
profile arc-admin

@app
serverless-upload-arc

@http
get /
post /upload # new POST route

@plugins
architect/plugin-storage-public # enables public buckets
ticketplushq/arc-plugin-s3rver # runs a local s3 server

@storage-public
FileUploadBucket # creates a public bucket
import awsLite from '@aws-lite/client'
const s3Config = JSON.parse(process.env.ARC_S3RVER_CONFIG || '{}')
const aws = await awsLite({ ...s3Config, plugins: [ import('@aws-lite/s3') ] })

export async function handler(req) {
    try {
        const body = JSON.parse(req.body);
        const decodedFile = Buffer.from(body.file, 'base64');
        const params = {
            "Body": decodedFile,
            "Bucket": process.env.ARC_STORAGE_PUBLIC_FILEUPLOADBUCKET,
            "Key": body.filename,
            "ContentType": body.contentType
        };
        await aws.s3.PutObject(params).then(() => console.log('congrats!'))
        return {
            statusCode: 200,
            body: JSON.stringify({ message: "Praise Cage!" }),
        };
    } catch (err) {
        console.error(err);
        return {
            statusCode: 500,
            body: JSON.stringify({ message: "Error uploading file", error: err.message }),
        };
    }

}