Front-End Web & Mobile

Fullstack TypeScript: Reintroducing AWS Amplify

We are thrilled to announce the general availability of AWS Amplify Gen 2, a fullstack TypeScript experience for building cloud-connected apps. AWS Amplify helps you accomplish two jobs:

  1. Host your web app
  2. Build and connect to a cloud backend

With Amplify Gen 2, every part of your app’s cloud backend is defined in TypeScript. Need an Auth backend? TypeScript. Data backend? TypeScript. Storage backend? TypeScript. Everything is defined in TypeScript. What’s not changing? Amplify is built by and on AWS, giving you the ability to add any of the 200+ AWS services when you need to. Including generative AI services such as Amazon Bedrock? You guessed it: TypeScript.

All week, we’re going to be shipping new Gen 2 features as part of our launch week. Today we’ll cover:

  1. Deploy a server-side rendered or single-page application with a brand new Amplify console
  2. Zero-config authentication and authorization
  3. Fully type-safe cloud data integration in your frontend app
  4. PubSub APIs for real-time multiplayer use cases

P.S. If you’ve used Amplify in the past and it wasn’t quite for you, I highly encourage you to try again. Your feedback inspired us to build Gen 2.

Building with Amplify Gen 2: A brand new DX

We are developers too, and know that the best way to show you what’s new is to build something together. Let’s take a quick tour on how to build a fullstack TypeScript application with Amplify. Let’s build a real-time multiplayer application that shows other users’ cursors in real-time. I’ve already created a sample repo that you can use to get started.

Demo of a multiplayer shared cursor application

Deploy an App Frontend in Clicks

Amplify Hosting enables you to ship your apps built in your favorite frameworks to your end users with zero configuration code. In this demo, we’ll walk through deploying a static Vite app, but keep in mind that you can deploy various types of web applications, including server-side rendered, static sites, or single-page applications (SPAs) built with popular frameworks like Next.js, Nuxt, and Astro using Amplify Hosting.

Step 1: Click here to create a new repo with this GitHub template as a starting point

Demo of how to use a GitHub template

Step 2: Click here to host the cloned GitHub repository’s app with Amplify

Demo of how to host a web app on AWS Amplify

While the deployment is ongoing, clone the repository locally to browse through the file system.

Demo how to get git clone URL

git clone <YOUR_GITHUB_URL>/amplify-social-room.git

Let’s take a look at the repo’s contents. You can see this is a standard React Vite application, with your frontend in the src/ folder and Amplify Gen 2’s backend in the amplify/ folder. You can see your auth resources defined in amplify/auth/resource.ts and your data resources defined in amplify/data/resource.ts.

Amplify’s CI/CD pipelines reads these definition files and creates corresponding cloud resources to facilitate user sign-up and sign-in, as well as creating a real-time API to create, read, update, and delete database contents.

To connect to these resources from the client-side, we’ll use Amplify’s client library. The client library will be configured using your backend deployment outputs to understand what the deployed API endpoints are. Don’t worry we’ll show you how to automate this later.

Go to the deployed backend resources tab and select “Download amplify_outputs.json”.

Demo of how to download amplify_outputs.json

Now move the amplify_outputs.json file to your app’s repository root. Your folder structure should look something like this:

├── amplify # Your Amplify backend definition files
│   ├── backend.ts
│   ├── auth # Auth backend definition
│   │   └── resource.ts
│   ├── data # Data backend definition
│   │   └── resource.ts
│   ├── package.json
│   └── tsconfig.json
├── index.html
├── package.json
├── amplify_outputs.json # ADD YOUR amplify_outputs.json HERE
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── main.tsx
│   └── ... # other frontend files
└── ... # other project template files

Next, install any dependencies locally by running the following command in your Terminal. This includes the dependencies for defining your backend and client libraries to connect to your backend.

npm install

Next, go to the src/main.tsx file and import the Amplify library and configure it with the outputs file:

import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';

Amplify.configure(outputs);

AWS rolls your Auth, so you don’t have to

We all know, we “shouldn’t roll our own auth”; that’s why we made it less than 10 lines of code to setup Auth with a fully AWS-managed auth service. Let’s take a look at integrating Amplify Gen 2’s auth features. In the amplify/auth/resource.ts file, we’ve already configured an auth backend that supports email login.

import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    email: true,
  },
});

You could call the Auth backend just using standard signIn and signUp APIs, but that would require you to build entire login UIs by hand. There’s also an even easier way! Amplify offers the “Authenticator” component, which provides a UI directly for you. You can fully customize the UI to look exactly like your brand.

In the src/main.tsx file, import the Authenticator component and its UI styles and then wrap your <App /> component with the <Authenticator/> component.

import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css'
// other imports


Amplify.configure(outputs);

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Authenticator>
      <App />
    </Authenticator>
  </React.StrictMode>,
)

Now in your Terminal, run the following command to start your localhost server:

npm run dev

You should now have a fully functional and customizable auth flow. Try to sign up and sign in as a user.

Demo showing the Amplify Authenticator working

Create a real-time cloud API: we’ll handle the WebSockets, database, and authorization

Now let’s look at the application data, let’s use first get the basic CRUDL use cases out of the way. In your amplify/data/resource.ts file, you can see a “Room” data model is already pre-created for you. We will reuse the data model TypeScript types within our client library. This ensures that your frontend stays in-sync with your backend definition.

const schema = a.schema({
  Room: a.model({
    topic: a.string(),
  }),
})

Let’s go to your <RoomSelector/> component, to now give users the ability to list and create different rooms.

First, generate a “Data client”, which allows you to access the backend data in a fully end-to-end type-safe way. Add the following below the imports in the src/RoomSelector.tsx file:

import { type Schema } from "../amplify/data/resource";
import { generateClient } from "aws-amplify/data";

const client = generateClient<Schema>();

The Schema type is a type exported by the amplify/data/resource.ts, which includes type suggestions for the Data client. Every data model in Amplify Gen 2 is real-time capable by default. So you get subscription events for record creation, updates, and deletions.

We’re also offering an observeQuery() capability that automatically subscribes to these events and returns a live list. Go to the RoomSelector’s useEffect function and add in the following code to ensure a live list of all available rooms is always available as options to select into:

    // set up a live feed inside the useEffect
    const sub = client.models.Room.observeQuery().subscribe({
      next: (data) => {
        setRooms([defaultRoom, ...data.items])
      }
    })
    return () => sub.unsubscribe()

Next, set the onClick handler on the [+ add] button. Once a room is successfully added, switch the end user to that newly created room.

My favorite TypeScript feature is IntelliSense auto-completion, and it’s one of the biggest reasons we chose to base Amplify on TypeScript. We were very intentional about making sure this was a key part of Gen 2. Here’s a GIF showing this, but also as you do the tutorial I strongly encourage you to type everything out so that you can experience it for yourself.

 

Your button component should look something like this:

    <button onClick={async () => {
      const newRoomName = window.prompt("Room name")
      if (!newRoomName) {
        return
      }
      const { data: room } = await client.models.Room.create({
        topic: newRoomName
      })
      
      if (room !== null) {
        onRoomChange(room.id)
      }
    }}>[+ add]</button>

Go to localhost in your browser and try it out for yourself now. Create a new room and switch between them. See how the dropdown options are automatically populated.

Demo of creating and joining a room

Cloud sandboxes: per-developer isolated environments for backend iteration

In Amplify Gen 2, you can use your own cloud sandbox that allows for faster iteration with the localhost instance of your frontend. The best way to set up an AWS account on your machine for local development is using AWS IAM Identity Center.

Pre-requisite: Follow this guide to setup AWS on your machine for local development.

Once your machine is fully set up, create a new Terminal session (running concurrently to npm run dev), and run the following command to deploy your cloud sandbox:

npx ampx sandbox

This will likely take a few minutes on the first-run as its deploying net new cloud resources but as you update existing resources, we’ll “hotswap” them to quickly reflect the cloud sandbox with your local changes. When your sandbox finishes deploying, you’ll see the contents in “amplify_outputs.json” replaced with the new backend endpoints.

We know how much developers love faster deployments. Amplify has contributed more than a dozen PRs to AWS CDK to allow “resource hotswaps” and in general reduced our deployment speeds on large schemas by a factor of 2x. There’s still more work to be done here but our goal is to continuously improve on our deployment speeds.

Create a multiplayer PubSub API to share cursors

In your amplify/data/resource.ts file, let’s add a couple new mutations and subscriptions to create a PubSub interface. Add the following code to your schema:

// const schema = a.schema({
  // Room: a.model(...), // Copy/paste the contents below the "Room" model
  publishCursor: a.mutation()
    .arguments(cursorType)
    .returns(a.ref('Cursor'))
    .authorization(allow => [allow.authenticated()])
    .handler(a.handler.custom({
      entry: './publishCursor.js',
    })),
  
  subscribeCursor: a.subscription()
    .for(a.ref('publishCursor'))
    .arguments({ roomId: a.string(), myUsername: a.string() })
    .authorization(allow => [allow.authenticated()])
    .handler(a.handler.custom({
      entry: './subscribeCursor.js'
    })),
// })
  

This defines a publishCursor mutation and a subscribeCursor subscription whenever the mutation gets called.

Next, let’s create the handlers for the mutation and subscriptions. Create a new amplify/data/publishCursor.js file with the included contents:

export const request = () => ({ })

export const response = (ctx) => ctx.arguments

This handler effectively says “just pass whatever arguments are provided through as the result” and that there’s no need to call any external data source.

Next, create a subscription handler file: amplify/data/subscribeCursor.js. This file should include some logic to filter out events for an end-user if a/ they’re not in the same room or b/ it’s their own event:

import { util, extensions } from '@aws-appsync/utils'
export const request = () => ({ });

export const response = (ctx) => {
    const filter = {
         roomId: { eq: ctx.arguments.roomId },
         username: { ne: ctx.arguments.myUsername }
    }
    extensions.setSubscriptionFilter(util.transform.toSubscriptionFilter(filter))
    return null;
}

Now, let’s switch to the client-side code. In the src/CursorPanel.tsx file, add a subscription for the cursor events within the useEffect:

  useEffect(() => {
    // Add subscriptions here
    const sub = client.subscriptions.subscribeCursor({
      roomId: currentRoomId,
      myUsername: myUsername
    }).subscribe({
      next: (event) => {
        if (!event) { return }
        if (event.username === myUsername) { return }

        setCursors(cursors => {
          return {
            ...cursors,
            [event.username]: event
          }
        })
      }
    })

    return () => sub.unsubscribe()
  }, [myUsername, currentRoomId])

Next, update the debouncedPublish function to call the publish mutation. We’ve added a bit of a throttle logic to not send too many events at once:

    const debouncedPublish = throttle(150, (username: string, x: number, y: number) => {
      client.mutations.publishCursor({ roomId: currentRoomId, username, x, y })
    }, {
      noLeading: true
    })

Now if you open the localhost site in two different windows, you should see a live cursor stream through across the different browser windows.

Demo showing the entire app working together

Ship on every git push

Because Amplify uses a Git-based CI/CD workflow, all you have to do to get this app shared with a URL is to commit this to git and push to the origin.

git commit -am "added multi-cursor capability"
git push

Now, you can check in the Amplify console when the build is ready. Once ready, share the URL with others to start playing with this multi-cursor application.

Demo of opening up the production URL

The elephant in the room: Gen 1 → Gen 2 migration.

Now that we’re at the end of the demo, I hope you’re excited to try out Amplify Gen 2. If you’re currently an Amplify Gen 1 customer, I’m sure you’re asking yourself “how do I migrate?”.

We are still actively developing migration tooling to aid in transitioning your project from Gen 1 to Gen 2. Until then, we recommend you continue working with your Gen 1 Amplify project. We’ve put together a Gen 1 vs. Gen 2 feature support matrix here. We remain committed to supporting both Gen 1 and Gen 2 for the foreseeable future. For new projects, we recommend adopting Gen 2 to take advantage of its enhanced capabilities. Meanwhile, customers on Gen 1 will continue to receive support for high-priority bugs and essential security updates. Follow AWS Amplify on X or join the Amplify Discord to keep up with the latest regarding Gen 1 to Gen 2 migrations.

Clean-up

  1. Go to the AWS Amplify console
  2. Go to your application
  3. Select “App Settings”
  4. Select “General Settings”
  5. Finally, select “Delete app”

It’s Day 1 for Fullstack TypeScript

Back in 2020, Chris Coyer from CSS-Tricks published this blog post on “ooooops I guess we’re* full-stack developers now”. The stack comprises of a few key components from frontend to the backend. Below is a slightly modified representation on what Chris described as the fullstack “spectrum“:

TypeScript is the popular programming language that spans across the spectrum from frontend to backend. The beauty of the TypeScript community is that your options are effectively endless. You can compose and recompose, pick and choose, adjust, modify and iterate on “just the right stack” for your use case.

I wanted to take a moment to acknowledge the amazing TypeScript communities out there. You should definitely also check out Next.js, Astro, Zod, ArkType, and tRPC just to name a few. Each of these libraries truly pushed the Fullstack TypeScript movement forward, enabling frontend developers to finally become: fullstack developers.

Feel free to use all of Amplify or just a subset of Amplify. The fullstack TypeScript movement is just starting. You are now in control of building “just the right” stack for your end users. To learn more about Amplify and to start building with it, visit our documentation guides!