Front-End Web & Mobile

Amplify Functions: Create serverless functions using TypeScript, powered by AWS Lambda

AWS Amplify is excited to announce the general availability of its Function offering for Gen 2. Amplify Functions are defined, authored, and consumed using TypeScript; whether they be a handler for your custom queries and mutations, or a trigger for your authentication resource. Under the hood, Amplify Functions are powered by AWS Lambda; however, Amplify enables you to deploy serverless functions in the same codebase and language as your app. Functions are used in a wide variety of use cases, but often enhance or modify the behaviors of your resources to build a tailored user experience for your apps.

In Amplify Gen 2, resources are defined in a resource file (i.e. resource.ts), whereas Functions are also created with a corresponding handler file (i.e. handler.ts). Functions give you the ability to iterate quickly by hot-swapping source code when you save the file. Get started with a TypeScript-based resource definition, a TypeScript-based handler, and each time your resource or handler file is saved your source code is bundled with esbuild and redeployed within seconds. With Amplify Functions, you do not need to worry about writing a tsconfig.json or build process for each Function; simply create the minimal resource definition and corresponding handler, then deploy.

The best part about functions is that they can be used with almost any resource! Some of the most frequent use cases we see customers using Amplify Functions for are:

  1. Connecting to resources not natively supported by Amplify, for example Amazon Bedrock for AI/ML
  2. Modifying the behavior of Amplify’s default authentication flow, like linking a UserProfile data record to a user
  3. Performing an event when an item is uploaded to a Storage bucket, for example resizing images
  4. Creating a resolver for custom queries and mutations on your data resource,

Here, we will be building a Function that interacts with Amazon Bedrock as a custom AWS resource in our Amplify app to generate a haiku based on the images stored in the cursor room. The Function will be attached to your Data resource as a handler for a custom query.

Defining a Function resource using TypeScript

To get started with Amplify Functions, you need a resource definition:

// amplify/functions/generateHaiku/resource.ts
import { defineFunction } from "@aws-amplify/backend"

export const generateHaiku = defineFunction({
  name: "generateHaiku",
})

And its corresponding handler (i.e. handler.ts) file:

// amplify/functions/generateHaiku/handler.ts
export const handler = async () => {}

With your personal cloud sandbox running, once you save the handler file a deployment will start for your first Function and deploy within seconds.

Use Functions with Amplify resources

In order for our function to read images from the storage resource, we must grant them access. Access is granted to other Amplify resources using common language, where Amplify simplifies IAM terminology to language appropriate for the resource such as read for Storage, or addUserToGroup for Auth. Let’s use the function’s resource definition to allow access to our Storage resource’s room/ path:

// amplify/storage/resource.ts
import { defineStorage } from "@aws-amplify/backend";
import { generateHaiku } from "../functions/generateHaiku/resource";

export const storage = defineStorage({
  name: 'gen2-multi-cursor-demo-app',
  access: allow => ({
    'room/*': [
      allow.authenticated.to(['get', 'write', 'delete']),
      allow.guest.to(['get', 'write', 'delete']),
      // grant the "generateHaiku" function "read" access
      allow.resource(generateHaiku).to(['read'])
    ]
  })
});

We’re building a function that is also attached as a resolver for a custom query in our data resource. Using the resource definition created earlier, let’s create the custom query:

// amplify/data/resource.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
import { generateHaiku } from '../functions/generateHaiku/resource';

const schema = a.schema({
  // ...
  generateHaiku: a.mutation()
    // specify a "roomId" argument
    .arguments({ roomId: a.string() })
    // we'll return the Haiku as a string
    .returns(a.ref('Haiku'))
    // authorize using an API key
    .authorization(allow => [allow.authenticated()])
    // specify the "generateHaiku" function we just created
    .handler(a.handler.function(generateHaiku))
    
}).authorization((allow) => [allow.authenticated()]);

// ...

By doing so, this also gives us a type-safe way to invoke the Function — using the generated Data client. Whenever our query is invoked with client.queries.generateHaiku() our Function will execute and have the ability to read from our Storage resource.

Use Functions with any AWS service

Amplify is built on top of the AWS Cloud Development Kit (CDK), meaning if you find yourself with a requirement to interact with an AWS service that Amplify does not provide first-class support for, you can use the CDK to extend or modify Amplify-generated resources. For example, we’re building a Function that uses data from Amplify resources to communicate with another AWS service, Amazon Bedrock:

// amplify/backend.ts
import { Stack } from "aws-cdk-lib/core"
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam"
import { defineBackend } from "@aws-amplify/backend"
import { auth } from "./auth/resource"
import { data } from "./data/resource"
import { storage } from "./storage/resource"
import { AMAZON_BEDROCK_MODEL_ID, generateHaiku } from "./functions/generateHaiku/resource"

/**
 * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
 */
const backend = defineBackend({
  auth,
  data,
  storage,
  generateHaiku,
})

// ...
// access the "generateHaiku" function from your defined backend
const generateHaikuFunction = backend.generateHaiku.resources.lambda

// use built-in CDK construct methods to extend the Function's role
generateHaikuFunction.addToRolePolicy(
  new PolicyStatement({
    effect: Effect.ALLOW,
    actions: ["bedrock:InvokeModel"],
    resources: [
      `arn:aws:bedrock:${
        Stack.of(generateHaikuFunction).region
      }::foundation-model/${AMAZON_BEDROCK_MODEL_ID}`,
    ],
  })
)

Expanded TypeScript Support

Now that we’ve seen how to define Amplify Functions and connect them to other resources, let’s take a look at how Amplify enhances the authoring experience for Functions with TypeScript.

Typed Environment variables

Amplify will generate narrowly-typed references for environment variables. If an environment variable is explicitly declared it will be available on the Function’s generated env reference. For example, when specifying the Amazon Bedrock model we’d like to use (to learn more about models, visit the AWS documentation for Amazon Bedrock):

// amplify/functions/generateHaiku/resource.ts
import { defineFunction } from "@aws-amplify/backend";

export const AMAZON_BEDROCK_MODEL_ID = "anthropic.claude-3-haiku-20240307-v1:0";

export const generateHaiku = defineFunction({
  name: "generateHaiku",
  environment: {
    AMAZON_BEDROCK_MODEL_ID,
  },
});

This environment variable becomes available on env using the key from environment :

// amplify/functions/generateHaiku/handler.ts
import { env } from "$amplify/env/generateHaiku"

console.log(env.AMAZON_BEDROCK_MODEL_ID)

Or, if you’ve specified access to another resource (like Storage), Amplify creates a reference to that resource’s metadata such as the Amazon S3 bucket name:

// amplify/functions/generateHaiku/handler.ts
import { env } from "$amplify/env/generateHaiku"

console.log(env.GEN_2_MULTI_CURSOR_DEMO_APP_BUCKET_NAME)

Typed Handler Functions

When attaching a function as a resolver to a custom query or mutation, Amplify provides a type specifically for your Function’s handler:

// amplify/functions/generateHaiku/handler.ts
import { env } from "$amplify/env/generateHaiku"
import { type Schema } from "../../data/resource"

// use the prebuilt handler type from your Schema
type Handler = Schema["generateHaiku"]["functionHandler"]

export const handler: Handler = async (event) => {
  // arguments are typed based on the query definition
  const { roomId } = event.arguments

  // to satisfy the handler's "returnType" requirement, return the Haiku
  return {
    content: "",
    roomId,
  }
}

Preconfigured TypeScript Builds

When creating an Amplify app with create-amplify , Amplify scaffolds a TypeScript project configuration file with modern settings and bundles Functions using esbuild. You do not need to worry about individual configurations per Function’s entrypoint, nor build scripts. Using esbuild and modern TypeScript project settings, Amplify enables you to import TypeScript files from other modules. In a monorepo setting, this enables you to modularize your business logic and re-use across Functions without needing to worry about compiling your packages’ TypeScript to JavaScript.

Secrets in Functions

Functions in Amplify remove the complexity of defining the relationship between secrets from another AWS services to the underlying AWS Lambda function, and the AWS SDK boilerplate required to resolve secret values at runtime. Secrets are either created through the CLI for use with personal cloud sandbox instances, or through the Amplify console for branch deployments.

With Amplify Sandbox, secrets are set using the CLI:

npx ampx sandbox secret set MY_SECRET

Secrets can then be bound to a Function’s environment using with secret():

// amplify/functions/generateHaiku/resource.ts
import { defineFunction, secret } from "@aws-amplify/backend"

export const generateHaiku = defineFunction({
  name: "generateHaiku",
  environment: {
    MY_SECRET: secret("MY_SECRET")
  },
})

And consumed just like environment variables:

// amplify/functions/generateHaiku/handler.ts
import { env } from "$amplify/env/generateHaiku"

console.log(env.MY_SECRET)

Get Started Today!

So far we’ve covered:

  • how to define an Amplify Function
  • how to use an Amplify Function as a resolver for custom queries and mutations
  • how to grant an Amplify Function access to another Amplify resource
  • how to grant an Amplify Function access to any AWS service
  • how to use environment variables and secrets in Amplify Functions
  • how to type our Amplify Function handlers and other fun TypeScript-related features

To see how these features come together in a practical setting, visit the day-3 branch of the app sample. To get started with Amplify Functions today, run npm create amplify@latest and visit Amplify’s documentation for Functions to learn more!