CLJS ❤️'s AWS Amplify
Tags: aws, clojure, javascript
Published: 2019-12-13 16:23:11

# A Brief introduction
AWS Amplify is a tool from Amazon's AWS for building applications atop AWS services like Cognito, DynamoDB, and API Gateway. Its design intent is to enable small teams to quickly build full-featured applications with nothing more than an AWS account, some configuration files, and application code written in front-end technologies i.e. Javascript, Java, or Swift. In this post I show how to get started building with Amplify using Clojurescript via Shadow-CLJS. This post will gloss over a lot of the details of the pieces it weaves together in service of showing how to get to the final product, a deployed web-app that uses a simple GQL powered DynamoDB backend.
## Notes:
This guide is adapted from the AWS Amplify docs located [here](https://aws-amplify.github.io/docs/js/start?platform=purejs), if you encounter problems you can look to that guide or reach out to me.

# Step 0: Pre-reqs
## Make an AWS Account
Because this is targeting AWS you will need an AWS account which you can create [here](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html)

## Setup the amplify CLI
Next is actually installing the command line toolchain (`amplify`). See the instructions [here](https://aws-amplify.github.io/docs/).

# Step 1: Setup the project
In order for us to actually use our tools we need to make sure that they have everything they need to run. Our goal is to lay our project out like this:
```
.
├── build
│   ├── app
│   ├── index.html
│   └── js
│       ├── main.js
│       └── manifest.edn
├── deps.edn
├── package.json
├── shadow-cljs.edn
├── src
│   ├── app.cljs
│   ├── aws-exports.js
│   └── graphql
│       └── schema.json
└── yarn.lock
```
This file tree contains configuration for `yarn`/`npm` (package.json), `shadow-cljs` (shadow-cljs.edn), and `clj` (deps.edn) [which is used by shadow to manage our Clojure/script dependencies] and our source files that will be transpiled into the Javascript that is our app.

To setup a rough skeleton of our project run:
```bash
$ mkdir -p amplify-js-app/src amplify-js-app/public && cd amplify-js-app
$ touch package.json shadow-cljs.edn deps.edn public/index.html src/app.cljs 
```

## Add the project configuration
### [Package.json](https://flaviocopes.com/package-json/)
```json
{
  "name": "amplify-js-app",
  "version": "1.0.0",
  "description": "Amplify JavaScript Example",
  "dependencies": {
    "@aws-amplify/api": "latest",
    "@aws-amplify/pubsub": "latest"
  },
  "devDependencies": {
   "shadow-cljs": "^2.8.58"
  },
  "scripts": { 
    "start": "shadow-cljs watch",
    "release": "shadow-cljs release",
    "server": "shadow-cljs server"
  }
}
```
### [shadow-cljs.edn](https://shadow-cljs.github.io/docs/UsersGuide.html)
```clojure
{:source-paths ["src"]
 :deps true 
 :nrepl {:port 64824} 
 :builds {:app {:target :browser
                :output-dir "build/js"
                :asset-path "/js"
                :modules {:main {:entries [app]}} ;; <- becomes public/js/main.js
                ;; start a development http server on http://localhost:8020
                :devtools {:http-root "build"
						   :http-port 8020}}}}

``` 
### [deps.edn](https://clojure.org/guides/deps_and_cli)
```clojure
{:deps {thheller/shadow-cljs {:mvn/version "2.8.58"}
		thheller/shadow-cljsjs {:mvn/version "0.0.12"}
        appliedscience/js-interop {:mvn/version "0.1.20"}
        binaryage/devtools {:mvn/version "0.9.10"}}
 :paths ["src"]}
```
Now that our app code can actually be built using our tools its time to invoke them.
## Install all the things
`yarn`

## The HTML that will be served
```html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Amplify Framework</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            html, body { font-family: "Amazon Ember", "Helvetica", "sans-serif"; margin: 0; }
            a { color: #FF9900; }
            h1 { font-weight: 300; }
            .app { width: 100%; }
            .app-header { color: white; text-align: center; background: linear-gradient(30deg, #f90 55%, #FFC300); width: 100%; margin: 0 0 1em 0; padding: 3em 0 3em 0; box-shadow: 1px 2px 4px rgba(0, 0, 0, .3); }
            .app-logo { width: 126px; margin: 0 auto; }
            .app-body { width: 400px; margin: 0 auto; text-align: center; }
            .app-body button { background-color: #FF9900; font-size: 14px; color: white; text-transform: uppercase; padding: 1em; border: none; }
            .app-body button:hover { opacity: 0.8; }
        </style>
    </head>
    <body>
        <div class="app">
            <div class="app-header">
                <div class="app-logo">
                    <img src="https://aws-amplify.github.io/images/Logos/Amplify-Logo-White.svg" alt="AWS Amplify" />
                </div>
                <h1>Welcome to the Amplify Framework</h1>
            </div>
            <div class="app-body">
                <button id="MutationEventButton">Add data</button>
                <div id="MutationResult"></div>
                <div id="QueryResult"></div>
                <div id="SubscriptionResult"></div>
            </div>
        </div>
        <script src="js/main.js"></script>
    </body>
</html>
```
With the HTML in place we actually need to build a `/js/main.js` which can be done one two ways, development aka `:optimizations :none` and production aka `:optimizations :advanced`. Because of [complicated reasons](https://shadow-cljs.github.io/docs/UsersGuide.html#js-provider) and compile time constraints we will just run the development builds locally.
## 
```
yarn run start app #This will produce a local, source-mapped build and start the shadow-cljs interal http server for us to test with.
```

# Step 2: The Backend
Up until now we have mostly concerned just our frontend code. Now we will configure a simple backend using `amplify` which takes care of a ton of the heavy lifting.

```bash
amplify init  #accept most defaults, naming your env dev
```

Output:
```text
➜ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project amplify-js-app
? Enter a name for the environment dev 
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  src
? Distribution Directory Path: build
? Build Command:  yarn run release app
? Start Command: yarn run start app
```
And then we can run `amplify status` to check that everything "worked" (there are no resources attached to our backend yet, it is just scaffolded out)

Output:
```
➜ amplify status

Current Environment: dev

| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | --------------- |
```

# Step 4
The next step is to add our GQL API. The only thing that we need to do is run `amplify add api`, answer a few questions, and wait. 

```text
➜ amplify add api   
? Please select from one of the below mentioned services: GraphQL
? Provide API name: amplifyJsApp
? Choose the default authorization type for the API API key
? Enter a description for the API key: 
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
```
Now that we have "added" the api to our backend we need to AWS to make those actual changes. This is a common theme throughout amplify, changes are made to local configuration files or, more commonly, through the `amplify` CLI, and then when those changes are ready to deployed we push them to AWS via `amplify push`.

```
➜ amplify push
...
? Are you sure you want to continue? Yes
...
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
...
✔ Generated GraphQL operations successfully and saved at src/graphql
✔ All resources are updated in the cloud

GraphQL endpoint: <snip>
GraphQL API KEY: <snip>

```
This command will ask us a few questions about if want leverage the [codegen facilities](https://aws-amplify.github.io/docs/cli-toolchain/graphql#codegen) that come with the Amplify CLI, we do.

Next we need to actually wire up the backend that we just deployed to the page we have created. We are going to base this code off the examples found in [here](https://aws-amplify.github.io/docs/js/start#step-4-integrate-into-your-app) in the Amplify docs. For brevity I won't break down what the source code is doing other than to say it is a very literal translation of the complete code in step 4.

```clojure
(ns app
  (:require ["@aws-amplify/api" :refer [graphqlOperation] :default API]
            ["@aws-amplify/pubsub" :default PubSub]
            ["/graphql/mutations" :refer [createTodo]]
            ["/graphql/queries" :refer [listTodos]]
            ["/graphql/subscriptions" :refer [onCreateTodo]]
            ["/aws-exports" :default awsconfig] ;; Important Diff, no period
            [applied-science.js-interop :as j]))

;;Setup/Config
(j/call API :configure awsconfig)
(j/call PubSub :configure awsconfig)

;;Mutations

(defn create-todo []
  (let [todo        (j/obj :name "Use AppSync"
                           :description "Realtime and Offline")
        gql-todo-op (graphqlOperation createTodo (j/obj :input todo))]
    (j/call API :graphql gql-todo-op)))

(def mutation-button (j/call js/document :getElementById "MutationEventButton"))
(def mutation-result (j/call js/document :getElementById "MutationResult"))

(j/call mutation-button :addEventListener
        "click" (fn [_] 
                  (j/assoc! mutation-result :innerHTML "MUTATION RESULTS:")
                  (-> (create-todo)
                      (.then (fn [evt]
                               (let [na       (j/get-in evt [:data :createTodo :name])
                                     descript (j/get-in evt [:data :createTodo :description])]
                                 (j/update! mutation-result :innerHTML str
                                            "<p>" na "-" descript "</p>")))))))

;; Queries
(def query-result (j/call js/document :getElementById "QueryResult"))
(defn get-data
  []
  (j/assoc! query-result :innerHTML "QUERY RESULTS:")
  (->  (j/call API :graphql (graphqlOperation listTodos))
       (.then (fn [evt]
                (let [todos (j/get-in evt [:data :listTodos :items])]
                  (mapv 
                   #(let [na       (j/get-in % [:name])
                          descript (j/get-in % [:description])]
                      (js/console.log %)
                      (j/update! query-result :innerHTML str "<p>" na " - " descript "</p>"))
                   todos))))))

(get-data)

;;Subscriptions

(def subscription-result (j/call js/document :getElementById "SubscriptionResult"))
(-> (j/call API :graphql (graphqlOperation onCreateTodo))
    (.subscribe (j/obj :next 
                       (fn [evt]
                         (j/assoc! subscription-result :innerHTML "SUBSCRIPTION RESULTS:")
                         (let [na       (j/get-in evt [:value :data :onCreateTodo :name])
                               descript (j/get-in evt [:value :data :onCreateTodo :description])]
                           (j/update! subscription-result :innerHTML str "<p>" na " - " descript "</p>"))))))
```

This app allows us to save a todo in DynamoDB by clicking that button, includes a list of the todo's that have been created, and finally have a live updating section that updates with last todo that was created.

# Step 5: Send it live

The Final step is for us to ship the app to "prod". For this we need to add the [`hosting` resource](https://aws-amplify.github.io/docs/cli-toolchain/quickstart#workflow-1) to our amplify backend which enables[S3 Bucket based website hosting](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html)
```
➜ amplify add hosting
? Select the environment setup: DEV (S3 only with HTTP)
? hosting bucket name amplify-js-app-20191210185143-hostingbucket
? index doc for the website index.html
? error doc for the website index.html

You can now publish your app using the following command:
Command: amplify publish
```

And then publish your app! (Note that it took about 5 minutes to upload all of the files in my build folder to S3 because we never cleaned up the CLJS runtime development files, this can be fixed by modifying the `script` in the `package.json`).
```
➜ amplify publish
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name   | Operation | Provider plugin   |
| -------- | --------------- | --------- | ----------------- |
| Hosting  | S3AndCloudFront | Create    | awscloudformation |
| Api      | amplifyJsApp    | No Change | awscloudformation |
? Are you sure you want to continue? Yes
...
✨  Done in 18.70s.
frontend build command exited with code 0
✔ Uploaded files successfully.
Your app is published successfully.
```

#🎉🎉🎉
And Done! If you have any questions or feedback reach out on [Twitter](https://twitter.com/royalaid), or @royalaid on the Clojurians [Slack](https://clojurians.slack.com/)