Front-End Web & Mobile

Building fast Next.js apps using TypeScript and AWS Amplify JavaScript v6

We are excited to announce the general availability of v6 of the AWS Amplify JavaScript Library. This release has many of the most asked for improvements and features that you, our community, have been asking for. This release provides substantial reductions to bundle size, improved TypeScript coverage and typing support, secure runtime token support, and full support for Next.js App Router and Server Actions.

Faster App Load Times and Smaller Bundle Sizes

Speed is not a luxury; it’s a necessity. That’s why we’ve invested in reducing our dependencies, improving our tree shaking capabilities, and optimizing our architecture. Smaller bundles mean your applications load quicker, ensuring that you keep your users engaged and happy, whether they are on high-speed broadband or a patchy connection. These changes have enabled Amplify to be optimized for the most commonly used frameworks and build tools.

When building a new React app using (create-react-app) and comparing the bundle size of all APIs in a category, we see the following bundle size reduction compared to v5:

Auth: 55kb to 32kb (42%)
Storage: 38kb to 21kb (45%)
Analytics (Pinpoint): 31kb to 18kb (42%)
Notifications and Analytics: 39kb to 23kb (41%)
API REST & GraphQL: 91kb to 38kb (58%)

Graph that shows bundle size improvements with AWS Amplify JS V6 Library

NOTE: The size numbers measure the final minified and gzipped bundle sizes. The “After” results were generated using Amplify JavaScript v6.0.2. The “Before” results were generated using Amplify JavaScript v5.3.4 when the lightweight clients were introduced and Amplify JavaScript v5.2.4 prior to those improvements.

In Amplify JS v6, with the introduction of functional APIs and improved tree-shaking capabilities, only the APIs you import into your app contribute towards the bundle size and unused functionality is tree-shaken away. For example, in an app that only uses only subset of APIs from Auth (signInWithRedirect, signOut, fetchAuthSession, getCurrentUser) and Storage (uploadData, downloadData), we see a 59% reduction in bundle size from 77kb in v5 to 31kb in v6.

Graph that shows Auth Hosted UI + Storage bundle sizes

Elevating the TypeScript experience

TypeScript has become essential to many teams’ development workflow, offering a level of type-safety that makes larger and more complex projects manageable. Amplify’s JavaScript library’s full public API surface now has intuitive types that simplify usage. With these TypeScript enhancements, you’ll get richer syntax highlighting and code completion in your text editor. Type checking will help you identify some bugs before you even need to run your app.

Let’s take a look at how you may want to use the new generateClient API to query a product from an AWS AppSync API. In this example we’ll use the new generateClient api to run a mutation to make an update to a to do in a to do list app. You may notice you no longer need to set the graphql API type, it’s automatically inferred for you.

Shows graphql comamnd

We’ve received feedback that we need better types for logged in users, so we created the new getCurrentUser API that returns a fully type user object! Let’s use it to grab a loginId under the signInDetails.

Shows types on user

Supporting Next.js App Router, API routes, and middleware

Amplify’s JavaScript library now supports all Next.js capabilities in a new *Next.js adapter*. This enables you to use Server-Side Rendering, App Router with React Server Components, or even use API routes with middleware to control access to only authenticated users. Using Amplify JavaScript v6, you can run Amplify on any of the available Next.js runtimes and use any rendering option (SSR, ISR, or static).

The Next.js adapter allows you to run the Amplify library inside an “Amplify Server Context”, which offers you a secure way to use the Amplify library functionality in the cloud. Once the functionality of Amplify finishes running, the context is completely destroyed, which eliminates any cross-request contamination requests, elevating the security posture of your apps.

Let’s take a look a few scenarios using the App Router and the Pages router in Next.js.

Prerequisites

Install the latest Amplify libraries including the Next.js adapter to get started:

npm i aws-amplify @aws-amplify/adapter-nextjs

Go through the Amplify getting started guide if you haven’t in the past. You should have an GraphQL API and an auth API set up by the end of the tutorial.

App Router

In our first scenario let’s imagine we are using a Next.js application using the App router. We want to use one of our server components to connect to our AWS AppSync API and list out some data.

Let’s begin by creating a new utility file that will have the serverClient function that we can use to talk to our Amplify APIs on the server side.

// utils/server-utils.ts
import { cookies } from "next/headers";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";

import config from "../../amplifyconfiguration.json";

export const serverClient = generateServerClientUsingCookies({
  config,
  cookies,
}); 

The generateServerClientUsingCookies function generates an API that can be used with Next.js server components with dynamic rendering. This creates a secure way to access Amplify APIs on the server side. The serverclient is exported so it can be used as a utility function to call our API. In our page.tsx file we will import this function and use it to list some data for our to do app.

// page.tsx
import { serverClient } from "@/utils/server-utils";
import * as query from "@/graphql/queries";

export default async function Home() {
  const { data, errors } = await serverClient.graphql({
    query: query.listTodos,
  });
  
  if(errors){
   // handle errors
  }
  
return (
    <div>
      {data.listTodos.items.map((post) => {
        return (
          <li key={post.id}>
            <div>Name: {post.name}</div>
            <span>Description: {post.description}</span>
          </li>
        );
      })}
    </div>
  );

Let’s add a delete so users can delete a post:

// page.tsx
import * as mutations from "@/graphql/mutations";
import { serverClient } from "@/utils/server-utils";
...

async function deletePost(formData: FormData) {
    "use server";
    const id = formData.get("postId")?.toString();

    if (id) {
      const { errors } = await serverClient.graphql({
        query: mutations.deleteTodo,
        variables: {
          input: {
            id,
          },
        },
      });
      if (errors) {
        // handle errors
      }
    }
  }

This delete action uses Server Actions to retrieve a postId and then delete it. This allows us to do a form submit on the server and grab the id so it can be passed to the input variables.

It’s worth mentioning to get the correct TypeScript inference you must have your types generated. This includes an API.ts file that will be generated in the src folder and the mutations file in the graphql folder. You can do this by running the amplify add code command in your root folder.

Pages Router

If we are using the pages router in Next.js, we can use the new createServerRunner and generateServerClientUsingReqRes to run queries on the server.

Let’s begin by creating a utils file with two functions.

  • runWithAmplifyServerContext will be used to run the Amplify APIs in an isolated matter on the server.
  • serverGraphQLClient will be used to talk to our GraphQL APIs in middleware, API routes, getServerSideProps or getStaticProps.
// utils/server-utils.ts
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import config from "../../amplifyconfiguration.json";
import { generateServerClientUsingReqRes } from "@aws-amplify/adapter-nextjs/api";

export const { runWithAmplifyServerContext } = createServerRunner({
  config,
});

export const serverGraphQLClient = generateServerClientUsingReqRes({
  config,
});

Let’s take a look at how this would work using getServerSideProps. We will imagine a scenario where you’d like to get the signed in user’s information.

// index.tsx
import { runWithAmplifyServerContext } from "@/utils/server-utils";
import { getCurrentUser } from "@aws-amplify/auth/server";

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
  const currentUser = await runWithAmplifyServerContext({
    nextServerContext: { request: req, response: res },
    operation: async (contextSpec) => getCurrentUser(contextSpec),
  });

  return { props: { currentUser } };
};

As seen above, you can get the signed in user information on the client side using getCurrentuser. However, we can also get this same information on the server side using a slightly different version with a new import path of "aws-amplify/auth/server". This server version of getCurrentUser requires us to pass a contextSpec over.

In another scenario we could also use the servergraphQLClient function to retrieve a list of todos.

// index.tsx
import { listTodos } from "@/graphql/queries";
import { runWithAmplifyServerContext, serverGraphQLClient } from "@/utils/server-utils";

export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
  const todoList = await runWithAmplifyServerContext({
    nextServerContext: { request: req, response: res },
    operation: (contextSpec) =>
      serverGraphQLClient.graphql(contextSpec, {
        query: listTodos,
      }),
  });

  return { props: { todoList } };
};

The serverGraphQLClient will use the graphql API, which will take in a contextSpec. Here we can also pass a list of todos in gql format.

Middleware

Amplify also now supports middleware in Next.js. You can use the runWithAmplifyServerContext inside middleware to work with Amplify APIs.

Let’s build an app where you want to redirect to the login route any time a user is not authenticated. Here is an example using the app router. First, we’ll create a new server-utils file in the utils folder:

// utils/server-utils.ts
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import config from "../../amplifyconfiguration.json";

export const { runWithAmplifyServerContext } = createServerRunner({
  config,
});

This will create the runWithAmplifyServerContext that we’ll be using in our middleware below.

// middleware.ts
import { runWithAmplifyServerContext } from "@/utils/server-utils";

// The fetchAuthSession is pulled as the server version from aws-amplify/auth/server
import { fetchAuthSession } from "aws-amplify/auth/server";
import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // The runWithAmplifyServerContext will run the operation below
  // in an isolated matter.
  const authenticated = await runWithAmplifyServerContext({
    nextServerContext: { request, response },
    operation: async (contextSpec) => {
      try {
        
        // The fetch will grab the session cookies
        const session = await fetchAuthSession(contextSpec, {});
        return session.tokens !== undefined;
      } catch (error) {
        console.log(error);
        return false;
      }
    },
  });

  // If user is authenticated then the route request will continue on
  if (authenticated) {
    return response;
  }

  // If user is not authenticated they are redirected to the /login page
  return NextResponse.redirect(new URL("/login", request.url));
}

// This config will match all routes accept /login, /api, _next/static, /_next/image
// favicon.ico
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!api|_next/static|_next/image|favicon.ico|login).*)",
  ],
};

You’ll notice we are using the new fetchAuthSession API this will allow us to retrieve the tokens to confirm if a user is signed in or not. If the user is not signed in it will redirect to the login page.

Try out Amplify JavaScript v6

We’re thrilled to deliver this release addressing common requests from the Amplify community. By optimizing bundle sizes, Amplify aims to enable fast load times for all users regardless of connectivity. Improved TypeScript support enriches the coding experience and reduces errors. The Next.js integration allows you to use all its available features.

We are excited to see what you build with these new capabilities! You can visit the Amplify JavaScript documentation to explore all the features of JSv6.