How To Create A Fullstack TypeScript App Using AWS Amplify Gen 2
Tags: webdev, javascript, amplify, typescript
Published: 2024-05-31 19:06:37

[AWS Amplify Gen 2 ](https://docs.amplify.aws/)is a brand new way to create fullstack applications on AWS. In this tutorial, we’ll explore how to get started creating a fullstack TypeScript application with Next.js! We’ll add storage and then connect it to our Amplify connected UI component library to upload files.

With  Gen 2 we focused our efforts on creating a great developer experience. We wanted to make sure that you could focus on your frontend code instead of worrying about setting up your backend infrastructure. In this new version we created a brand new TypeScript safe DX that supports fast local development, and is backed by AWS Cloud Development Kit (CDK).

We recently released Gen 2 for general availability. Feel free to let us know what you think and leave a comment below.

With that said, before you get started makes sure your familiar with TypeScript, VSCode and Git. You'll also need a GitHub account and make sure Git is installed locally on the command line. We'll be using Next.js, however you don't need to be an expert in React to try out this tutorial.


## What Are We Building Today?

We will be creating an app that allows you to create todos with pictures. You’ll be using the [Amplify Next Starter](https://github.com/aws-samples/amplify-next-template) template and have it deployed to the AWS Amplify console. We'll create and connect to AWS AppSync, our managed GraphQL service, and use [Amazon Cognito](https://aws.amazon.com/pm/cognito), our customer identity and access management service, to authenticate and authorize users. Additionally, we'll use S3 for file storage of our images.

In this tutorial I'll walk you through step-by-step on how to create a new AWS Amplify Gen 2 app using the console, cloning that app, opening it in VSCode and adding in some additional features. We'll then deploy our changes back to the Amplify Gen 2 console to our hosted environment.

## Setup

We need to first have an AWS account ready. If you are not signed up, try out the [AWS Free Tier](https://aws.amazon.com/free/)! This will give you plenty of free resources to try out AWS and many of it's services. Keep in mind, all the services we'll work with today are on-demand and you'll only get charged when you are using them. 

We will be using the Next.js Amplify [starter template](https://github.com/new?template_name=amplify-next-template&template_owner=aws-samples&name=amplify-next-template&description=My%20Amplify%20Gen%202%20starter%20application) as described in the [Quickstart guide](https://docs.amplify.aws/react/start/quickstart/). Clone the repository using the Amplify Next.js Template into your own Github account to get started. 

Afterwords, sign into the [AWS management console](https://signin.aws.amazon.com/signin). Search at the top for AWS Amplify, and click it.

![AWS console choosing Amplify button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j6wdqz81rm6okvxkckhu.png)

In the Amplify Console choose Create new app.


![AWS console clicking create new app button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q38cj20yqzoisaiyr6j9.png)

Select GitHub as the Git provider.

![AWS console selecting the GitHub provider button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bt3abc1jiwtvt5r745ya.png)

In the next page you'll see a popup asking to connect to your Github account. This will give Amplify access to your account so it can connect to the Next.js repo you just created in the previous step from the starter template.

Select your app from the App name field and click next. If you don’t see your app click the `Update Github permissions` button and re-authorize with Github. On the next page leave all the default settings and click next. On the last page click `Save and deploy`. Amplify will now begin to deploy your new app!

The deployment may take several minutes. It will host your Next.js frontend and create infrastructure for your Amazon Cognito and AWS AppSync service backend. We’ll discuss this more later, and how we can make updates and changes to it. Until then, let's jump into VSCode and setup the frontend!


## Updating the frontend

Let's take a look at our app, and make some updates.

Open up your favorite terminal and clone the Github repo you just created in the previous section.

```
git clone <The_Copied_Githhub_URL>
```

Open up VSCode to that cloned project and open a terminal to the project’s root folder. Run the following command.


```bash
npm i @aws-amplify/ui-react-storage
```

This will install the [Amplify UI component library](https://ui.docs.amplify.aws). We'll use this later to add a file uploader, called Storage Manager, to our frontend to help with uploading pictures. 

If you look at the app folder structure you'll notice an amplify folder. This folder houses all the resources and files that will help us connect to our backend.

![List of folders for amplify](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w1ng7hn1yxa8f8f0f4tk.png)

By convention, the starter template will create an Amazon Cognito instance, and configure it for email login. The configuration for this will be in the resources file at `amplify/auth/resource.ts`. For more information on the auth resource please checkout the [docs](https://docs.amplify.aws/react/build-a-backend/auth/).

It also creates an AWS Appsync instance with a `Todo` schema in the `amplify/data/resource.ts` file.

Update the `data/resource.ts` to match the code below. The only difference is we are adding a new key to the model.

```typescript
// amplify/data/resource.ts

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
      key: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "apiKey",
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});
```

This schema does a few things.

* Adds a new Todo model with all the resolvers needed CRUDL (Create, Read, Update, Delete, List) and connects it to a new DynamoDB instance.
* The Todo model will have a content and a key  fields - both are strings.
* Adds public authorization rules so all users can create, read, update and delete, as long as they have the public api key.
* Sets a default authorization mode of apiKey and sets the expiration for 30 days. This is the public key.
* Exports a Schema that can be used by the frontend to ensure type safety end-to-end.

This resource data pattern is very powerful by allowing the user to completely configure the backend data schema all in code using TypeScript.

For the sake of this tutorial we will leave the default resource file unchanged for auth in the `amplify/auth/resources.ts` file.


## Adding Storage

We need to be able to upload images and retrieve them. We can do this using the Amplify Storage Category. 

Create a new folder at `amplify/storage`. Inside this folder create a `resource.ts` file. Copy the following code.

```typescript
// amplify/storage/resource.ts

import { defineStorage } from "@aws-amplify/backend";

export const storage = defineStorage({
  name: "todosStorage",
  access: (allow) => ({
    "media/*": [
      allow.guest.to(["read", "write", "delete"]), // additional actions such as "write" and "delete" can be specified depending on your use case
    ],
  }),
});
```

This will create a public bucket for users to upload our images too. Users will be able to upload images to the `media` folder and they’ll have access to `read`, `write`, and `delete` them. 

Next, let’s add this storage resource to our `backend.ts` file. Open the `backend.ts` file and add a new `storage` import. The complete file is below.

```typescript
//amplify/backend.ts

import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource.js";
import { data } from "./data/resource.js";
import { storage } from "./storage/resource.js";

defineBackend({
  auth,
  data,
  storage,
});
```

All our backend resource should be ready to go!


### Starting the Sandbox

To test locally we'll create a new ephemeral environment using Amplify tooling.

*If you've never setup AWS on the command line you'll need to run a few commands here to make sure your environment can connect to your account. Please follow the [Set up your AWS Account section](https://docs.amplify.aws/gen2/start/account-setup/) section in our docs before continuing on.*


```bash
npx ampx sandbox
```

This command will create an environment in AWS based on the resources you configured in your `amplify` folder. In this case it will create a Amazon Cognito, S3 Storage and AWS AppSync service. At any time you can stop this command, and it will delete all the resources it just created.

It will also create an `amplify_outputs.json` file in the root of your app. We'll need this file in the next section to setup our client `aws-amplify` library so it can talk to the backend services.

This will take a few minutes to run, after it completes continue onto the next section.

## Update page.tsx

The starter template has a built in todos app in it. Let’s modify the `page.tsx` file so we can upload files and connect them with our todos.

Inside the `page.tsx` app add an import for `StorageManager`, `StorageImage` and to our Amplify UI components library.

```typescript
// app/page.tsx
...

import { StorageImage, StorageManager } from "@aws-amplify/ui-react-storage";
import { Card, Flex, Text, Button } from "@aws-amplify/ui-react";
```

The UI components, `Card`, `Flex`, `Text` and `Button` are used to style our app. The `StorageImage` and `StorageManager` will allow us to add images and show them.

We need to be able to delete todos. Add the following function under the `listTodos()`.

```typescript
// app/page.tsx

...
 function deleteTodo(id: string) {
    client.models.Todo.delete({ id });
 }
```

We’ll need to update the `createTodo` function. It will now take in two arguments, `key` and `content`. 

```typescript
// app/page.tsx

...
  function createTodo({ key, content }: { key: string; content: string }) {
    client.models.Todo.create({
      content,
      key,
    });
  }
```

We’ll now do a few updates to the return statement. We’ll add a new `StorageImage` that will display our images. We’ll also add in the `StorageManager`. This will display a component so we can upload images.  

Update the return so it matches below.

```typescript
// app/page.tsx

return (
    <main>
      <h1>My todos</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} onClick={() => deleteTodo(todo.id)}>
            <Flex justifyContent="space-between">
              <Text>{todo.content}</Text>
              {todo.key ? (
                <StorageImage
                  path={todo.key}
                  alt={todo.content || ""}
                  width="100px"
                />
              ) : null}
            </Flex>
          </li>
        ))}
      </ul>
      <StorageManager
        path="media/"
        acceptedFileTypes={["image/*"]}
        maxFileCount={1}
        onUploadStart={({ key }) => {
          const content = window.prompt("Todos content");
          if (!key || !content) return;
          createTodo({ key, content });
        }}
        components={{
          Container({ children }) {
            return <Card variation="elevated">{children}</Card>;
          },
          FilePicker({ onClick }) {
            return (
              <Button variation="primary" onClick={onClick}>
                Add Todo and Choose File For Upload
              </Button>
            );
          },
        }}
      />
    </main>
  );
```

You may notice some props on the `StorageManager`. The components prop can be used to completely override the look and feel of the component. In this case we changed the container slightly to add a card around it. We also changed the `FilePicker` so we can use a different kind of button to upload.

We added an event listener called `onUploadStart`. This will trigger whenever an upload begins. It will first ask the user for the todos content. It will then call the `createTodo` which will create the todo in the database.

Here is the complete `app/page.tsx` file.

```typescript
"use client";
import { useState, useEffect } from "react";
import type { Schema } from "@/amplify/data/resource";
import { generateClient } from "aws-amplify/data";
import { StorageImage, StorageManager } from "@aws-amplify/ui-react-storage";
import { Card, Flex, Text, Button } from "@aws-amplify/ui-react";
import React from "react";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import "@aws-amplify/ui-react/styles.css";
Amplify.configure(outputs);
const client = generateClient<Schema>();

export default function App() {
  const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);

  function listTodos() {
    client.models.Todo.observeQuery().subscribe({
      next: (data) => setTodos([...data.items]),
    });
  }

  function deleteTodo(id: string) {
    client.models.Todo.delete({ id });
  }

  useEffect(() => {
    listTodos();
  }, []);

  function createTodo({ key, content }: { key: string; content: string }) {
    client.models.Todo.create({
      content,
      key,
    });
  }

  return (
    <main>
      <h1>My todos</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id} onClick={() => deleteTodo(todo.id)}>
            <Flex justifyContent={"space-between"}>
              <Text>{todo.content}</Text>
              {todo.key ? (
                <StorageImage
                  path={todo.key}
                  alt={todo.content || ""}
                  width="100px"
                />
              ) : null}
            </Flex>
          </li>
        ))}
      </ul>
      <StorageManager
        path="media/"
        acceptedFileTypes={["image/*"]}
        maxFileCount={1}
        onUploadStart={({ key }) => {
          const content = window.prompt("Todo content");
          if (!key || !content) return;
          createTodo({ key, content });
        }}
        components={{
          Container({ children }) {
            return <Card variation="elevated">{children}</Card>;
          },
          FilePicker({ onClick }) {
            return (
              <Button variation="primary" onClick={onClick}>
                Add Todo and Choose File For Upload
              </Button>
            );
          },
        }}
      />
    </main>
  );
}
```

## Trying it out

Go ahead and start the app.

```bash
npm run dev
```

The front page will load. Go ahead and click the `Add Todo` and `Choose File for Upload button`. 

![Storage Manager displayed with upload button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c1ki6ngzgswqy4htej60.png)


Choose a todo name and then choose a file. You’ll see the upload occur and a new todo displayed.


![Storage Manager with one amplify.png file uploaded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rc22f3z7xoisymgdyo01.png)


You can go ahead and add a few more. You can also click any todo for them to delete.

Good job! You've created a full stack app!

## Deploy to Production

In the first steps we created a new Next.js app using a starter template, we then cloned it down to edit it locally. We can now commit our changes and have it deployed back to our AWS Gen 2 console that will trigger a new branch build .

If you like, you can go ahead and stop the sandbox environment. Just go to the terminal where it's running and hit Ctrl/Cmd C. If for some reason you accidentally closed the terminal, you can always stop any sandbox environments by going back to the Amplify Gen 2 console and choosing the `Manage Sandboxes` button in the `All apps` page.

The sandbox environments are for testing only, and can be started or stopped at any time. The production environments are attached to your git branches that you pushed to Github. In this case the main branch is the production environment. If we choose to do so, we can create multiple environments if needed based on any branch we create and connect them to the AWS Gen 2 console.

To deploy our changes with the new frontend updates, all we'll need to do is a git commit and push. Open the terminal in your project and paste the following commands.

```bash
git add .
git commit -m "Updated todos App"
git push origin main
```

This will push all our changes to the Amplify Gen 2 console and trigger another build. After a few minutes click on the domain listed in the Amplify console to see it in action!

## Conclusion

In this tutorial we learned how to get started with AWS Amplify Gen 2. We used the Amplify Next.js starter template to create a new Amplify hosted app with our backend. We then made a todo app with photo storage, used the sandbox environment to test, and then pushed it to production to see the changes!

To clean up this environment, make sure to stop the sandbox environment and then go back into the amplify console, click on our app, then go to `App settings → General Settings`.


![App settings drop down](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl58t1jeaz5wswod88ys.png)

Click on the `Delete app` button.

![Delete app button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6tmm3zbepieery2qgix0.png)

If you'd like to learn more make sure to check our the [official docs](https://docs.amplify.aws)!