Serverless dependency management in OpenJS Architect
Tags: architect, aws, serverless, cloudnative
Published: 2021-03-08 17:23:50

[OpenJS Architect](https://arc.codes) is an open-source framework for writing and deploying serverless applications to AWS. Each Architect project is comprised of many small and independent Lambda functions that are executed statelessly in their own isolated runtimes. 

So how do we manage shared code and dependencies between functions? 

## Dependencies defined in your root `package.json`

You are likely to use some dependencies in each Lambda function that you are deploying like including the `@architect/functions` library. Architect has the ability to discover use your project's root `package.json` to install dependencies into your Lambda functions. Let's take a look at an example. 

First, start by creating a new Architect project 

```bash
npm init @architect ./arc-example
cd arc-example
npm install @architect/functions
```

By installing `@architect/functions` to the root `package.json` it will be available to all your functions when you require it. We won't need to manage dependencies with a per-function `package.json`.

So now we can use it in our `get-index` function.

```javascript
// src/http/get-index/index.js

let arc = require('@architect/functions')

async function route(req) {
  return {
    statusCode: 200,
    html: `<h1> Praise Cage </h1>`
  }
}

exports.handler = arc.http.async(route)
```
## `src/shared` and `src/views`
Another way we can handle shared code is with a special folder `src/shared`. Architect provides an easy way to abstract and reuse code in your functions. Most applications need to share logic, templates, or utilities. In order to do this, Architect uses a folder convention to copy the contents of `src/shared` and `src/views` into each function's `node_modules` directory. 

It's important to note that the entire contents of `src/shared` are copied recursively. I suggest you keep the directory structure as flat as possible, and the payloads as small as possible to improve performance. 

Let's create an example helper function. 

## `src/shared` example

To get started, create a new project from the command line. 
```bash
npm init @architect ./arc-shared-views
```

Next, we can modify the `app.arc` file in the root of the project with the following: 
```
# app.arc file
@app 
arc-shared

@http
get /
get /answer
```

Now we can start to build out our `src/shared` modules by creating a new folder at `src/shared/helper.js`. In this example, we need to make sure a number is converted to a string and this helper function will do the trick! 

```javascript
// src/shared/helper.js

function theAnswer() {
  //really important number that needs to be converted to a string
  let universe = 42 
  return universe.toString()
}

module.exports = theAnswer
```

We can use this helper in all of our functions by just requiring it from `@architect/shared`. Modify the get-answer function with the following: 

```javascript
// src/http/get-answer/index.js

let Answer = require('@architect/shared/helper')

exports.handler = async function http (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> The Answer is </title>
      </head>
      <body>
        <p> This is the Answer: ${Answer()} </p>
      </body>
      </html>
    `
  }
}
```

Run `npm start` from the command line and take a look at our code structure. Sandbox will hydrate our functions with a `node_modules/@architect/shared` directory which is part of the function's payload when deployed and executed.

```
.
├── src
│   ├── http
│   │   ├── get-index/
│   │   └── get-answer/
│   │
│   └── shared/
│       └── helper.js
│   
├── app.arc
└── package.json
```
When you navigate to http://localhost:3333/answer you will be greeted with data from our shared module, and it can be used by any other function.

## `src/views` example
The `src/views` folder is a special location that allows you to include code for each of your HTPP functions with a GET route. Continuing with our previous `src/shared` example we will include a layout template that your HTTP functions can use. 

Modify the `app.arc` file to match the following: 

```
@app
arc-shared

@http
get /
get /answer 
get /about
get /css/:stylesheet

@views
get / 
get /about
```

What we've done is added two new routes - `/about` and `css/:stylesheet`, then declared that two of the routes `/` and `/about` should receive a copy of the modules in `src/views`. 

Create a new folder and file, `src/views/layout.js`. In this file we'll write the following contents: 

```javascript
module.exports = function Layout (props) {
  props = props || {}
  let heading = props.heading || 'Architect views!'
  return `
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Architect example</title>
 <link rel="stylesheet" href="/css/styles.css">
</head>
<body>
 <h1>${heading}</h1>
</body>
</html>
`
}
```

This is our shared view template that will be used by each GET route listed under the `@views` pragma in the `app.arc` file. 

Next, we'll modify `src/http/get-index/index.js` with the following: 

```javascript
let Layout = require('@architect/views/layout')

exports.handler = async function http (request) {
  try {
    return {
      statusCode: 200,
      headers: {
        'content-type':'text/html; charset=utf8'
      }, 
      body: Layout()
    }
  } catch (e) {
    console.error(e)
    return {
      headers: {
        type: 'application/json; charset=utf8',
      },
      status: 500,
      body: JSON.stringify({
        name: e.name,
        message: e.message,
        stack: e.stack
      }, null, 2)
    }
  }
}
```

This function will call the layout file and return its output as the body of the response. Next, we can set up the about page to send a different set of props to the layout. Modify `sec/http/get-about/index.js` with the following: 

```javascript
let Layout = require('@architect/views/layout')

exports.handler = async function http (request) {
  try {
    return {
      statusCode: 200,
      headers: {
        'content-type':'text/html; charset=utf8'
      }, 
      body: Layout({heading: 'About'})
    }
  } catch (e) {
    console.error(e)
    return {
      status: 500,
      type: 'application/json; charset=utf8',
      body: JSON.stringify({
        name: e.name,
        message: e.message,
        stack: e.stack
      }, null, 2)
    }
  }
}
```

Now when `/about` is requested, this function will execute and be able to return the data being passed into `Layout()`.

Finally, we have some finder control over which GET functions will have `/src/views` copied into it. We do this with the `@views` pragma in the `app.arc` file. We want to create a URL to our style sheet, but this function doesn't need access to the layout code. Only the GET routes under `@views` will have the `src/views` code copied into it. Our first route of `/answer` won't have `src/views` modules copied into `node_modules`. 

Modify the code in `src/http/get-css-000stylesheet/index.js` with the following: 

```javascript
const styles = `
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
`
exports.handler = async function http (request) {
  return {
    statusCode: 200,
    type: 'text/css; charset=utf8',
    body: styles
  }
}
```
OK! Go ahead and run `npm start` from the project root and navigate to http://localhost:3333 to see our app in action. Change the route to http://localhost:3333/about and you'll see that our proper were passed as expected. 

See full example code in the repo: [https://github.com/pchinjr/arc-shared-views-example](https://github.com/pchinjr/arc-shared-views-example)