How To Create A Fullstack TypeScript App Using AWS Amplify Gen 2

erikch

Erik Hanchett

Posted on May 31, 2024

How To Create A Fullstack TypeScript App Using AWS Amplify Gen 2

AWS Amplify Gen 2 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 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, 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! 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 as described in the Quickstart guide. Clone the repository using the Amplify Next.js Template into your own Github account to get started.

Afterwords, sign into the AWS management console. Search at the top for AWS Amplify, and click it.

AWS console choosing Amplify button

In the Amplify Console choose Create new app.

AWS console clicking create new app button

Select GitHub as the Git provider.

AWS console selecting the GitHub provider button

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>
Enter fullscreen mode Exit fullscreen mode

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

npm i @aws-amplify/ui-react-storage
Enter fullscreen mode Exit fullscreen mode

This will install the Amplify UI component library. 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

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.

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.

// 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,
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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.

// 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
    ],
  }),
});
Enter fullscreen mode Exit fullscreen mode

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.

//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,
});
Enter fullscreen mode Exit fullscreen mode

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 section in our docs before continuing on.

npx ampx sandbox
Enter fullscreen mode Exit fullscreen mode

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.

// app/page.tsx
...

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

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().

// app/page.tsx

...
 function deleteTodo(id: string) {
    client.models.Todo.delete({ id });
 }
Enter fullscreen mode Exit fullscreen mode

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

// app/page.tsx

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

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.

// 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>
  );
Enter fullscreen mode Exit fullscreen mode

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.

"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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Trying it out

Go ahead and start the app.

npm run dev
Enter fullscreen mode Exit fullscreen mode

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

Storage Manager displayed with upload button

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

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.

git add .
git commit -m "Updated todos App"
git push origin main
Enter fullscreen mode Exit fullscreen mode

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

Click on the Delete app button.

Delete app button

If you'd like to learn more make sure to check our the official docs!

💖 💪 🙅 🚩
erikch
Erik Hanchett

Posted on May 31, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related