AWS Amplify Gen 2 — Ch.2: Anatomy of an Amplify Gen 2 Project

aws tutorial typescript web-dev

Video coming soon

Expected:

Creating Your First Project

Before examining the file structure, create a project to have something concrete to inspect. Start with a Next.js 15 app and add Amplify Gen 2:

# Create a new Next.js 15 project
npx create-next-app@latest my-amplify-app \
  --typescript \
  --app \
  --tailwind \
  --eslint \
  --src-dir

cd my-amplify-app

# Initialize Amplify Gen 2
npm create amplify@latest

# Install frontend Amplify library
npm install aws-amplify

The npm create amplify@latest command scaffolds the amplify/ directory and a set of starter files. Let’s walk through every file it creates and understand what each one does.

What command initializes an Amplify Gen 2 backend in an existing project?

The amplify/ Directory Structure

After initialization, your project has this structure:

my-amplify-app/
├── amplify/
│   ├── auth/
│   │   └── resource.ts        # Auth (Cognito) configuration
│   ├── data/
│   │   └── resource.ts        # Data schema (AppSync + DynamoDB)
│   ├── backend.ts             # Orchestrator — imports and wires everything
│   ├── package.json           # Amplify backend dependencies
│   └── tsconfig.json          # TypeScript config for the backend
├── src/
│   ├── app/
│   │   └── layout.tsx         # Root layout (where you'll add Amplify.configure)
│   └── ...
├── amplifyconfiguration.json  # Auto-generated — DO NOT edit manually
└── package.json               # Frontend dependencies

Notice there are two package.json files. The root one handles your Next.js frontend dependencies. The amplify/package.json handles backend-only dependencies — the packages that run on AWS Lambda and in the CDK synthesis environment, not in the browser.

The Orchestrator: backend.ts

amplify/backend.ts is the single source of truth for your entire backend. It imports resource definitions from subdirectories and passes them to defineBackend():

// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';

/**
 * @see https://docs.amplify.aws/react/build-a-backend/
 */
defineBackend({
  auth,
  data,
});

defineBackend() is the CDK construct factory. When you run npx ampx sandbox or trigger a cloud build, this file is the entry point. Amplify’s toolchain imports it, collects all the CDK constructs, synthesizes them into a CloudFormation template, and deploys it.

You can also directly access the underlying CDK stacks from backend.ts using backend.createStack() for adding custom AWS resources. This is how you escape Amplify’s abstractions when needed.

Resource Files: The resource.ts Pattern

Each Amplify resource lives in its own directory with a resource.ts file. The default amplify/auth/resource.ts looks like:

// amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';

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

And amplify/data/resource.ts starts with a Todo example:

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

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

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

The ClientSchema<typeof schema> type export is crucial. It derives TypeScript types from your schema definition at compile time. When you use generateClient<Schema>() on the frontend, TypeScript knows the exact shape of every model — field names, types, and whether they are required or optional.

What is the purpose of the `ClientSchema<typeof schema>` type export in the data resource file?

The Generated Configuration: amplifyconfiguration.json

After running npx ampx sandbox, Amplify generates amplifyconfiguration.json in your project root. This file is the bridge between your backend and your frontend. It contains all the endpoints, IDs, and configuration that the Amplify frontend library needs:

{
  "version": "1.3",
  "auth": {
    "user_pool_id": "us-east-1_XXXXXXXXX",
    "aws_region": "us-east-1",
    "user_pool_client_id": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "identity_pool_id": "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "mfa_methods": [],
    "standard_required_attributes": ["email"],
    "username_attributes": ["email"],
    "user_verification_types": ["email"],
    "mfa_configuration": "NONE",
    "password_policy": {
      "min_length": 8,
      "require_lowercase": true,
      "require_numbers": true,
      "require_symbols": true,
      "require_uppercase": true
    },
    "unauthenticated_identities_enabled": true
  },
  "data": {
    "url": "https://XXXXXXXXXXXXXXXXXXXXXXXXXX.appsync-api.us-east-1.amazonaws.com/graphql",
    "aws_region": "us-east-1",
    "default_authorization_type": "AMAZON_COGNITO_USER_POOLS",
    "authorization_types": [],
    "model_introspection": { ... }
  }
}

Important: Do not edit this file manually. It is regenerated every time you deploy. Add it to .gitignore if you are working on a team (each developer has their own sandbox with different IDs). However, the file generated for production should be committed for CI/CD builds.

Configuring the Frontend

On the frontend, Amplify.configure() reads amplifyconfiguration.json and initializes all the clients. In a Next.js 15 App Router project, the best place for this is a client component that wraps your layout:

// src/components/ConfigureAmplify.tsx
"use client";

import { Amplify } from "aws-amplify";
import config from "../../amplifyconfiguration.json";

Amplify.configure(config, { ssr: true });

export default function ConfigureAmplify() {
  return null;
}
// src/app/layout.tsx
import ConfigureAmplify from "@/components/ConfigureAmplify";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ConfigureAmplify />
        {children}
      </body>
    </html>
  );
}

The { ssr: true } option in Amplify.configure() is required for Next.js App Router. It tells Amplify to use cookies instead of localStorage for token storage, enabling server-side rendering to read the user’s auth state.

The Typed Data Client

With Amplify.configure() in place, you can create a typed data client anywhere in your app:

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

export const client = generateClient<Schema>();
// Using the client in a component
import { client } from "@/lib/amplify-client";

// TypeScript knows: content is string | null, isDone is boolean | null
const { data: todos } = await client.models.Todo.list();

// Create with full type safety
const { data: newTodo } = await client.models.Todo.create({
  content: "Learn Amplify Gen 2",
  isDone: false,
});

TypeScript will catch errors at compile time if you pass wrong field names or incorrect types. This is the “zero guesswork” benefit of generateClient<Schema>() — the type system knows your data model structure without any manual type definitions.

Dependencies: What Amplify Installs and Why

Amplify uses a split dependency model. The backend (amplify/package.json) contains:

{
  "dependencies": {
    "@aws-amplify/backend": "^1.0.0",
    "@aws-amplify/backend-cli": "^1.0.0",
    "aws-cdk-lib": "^2.0.0",
    "constructs": "^10.0.0",
    "typescript": "^5.0.0"
  }
}

The frontend (package.json) contains:

{
  "dependencies": {
    "aws-amplify": "^6.0.0",
    "@aws-amplify/ui-react": "^6.0.0"
  }
}

@aws-amplify/backend is for writing backend resource definitions and runs only during synthesis/deployment. aws-amplify is the browser-compatible library that runs in your frontend, providing Auth, Data, and Storage APIs.

Why does a Next.js Amplify project have two separate package.json files?

Test Your Knowledge

Conclusions

  • amplify/backend.ts is the single orchestrator — it imports and wires all resource definitions via defineBackend()
  • Each resource lives in its own directory with a resource.ts file following the defineXxx() convention
  • The TypeScript-to-CloudFormation pipeline: your source → CDK synth → CloudFormation → deployed AWS resources
  • amplifyconfiguration.json is auto-generated after each deploy — never edit it manually, do add it to .gitignore for team sandboxes
  • Amplify.configure(config, { ssr: true }) must be called once at the root of your Next.js app
  • generateClient<Schema>() creates a fully typed data client — TypeScript knows every field name and type from your schema
  • Two package.json files serve different purposes: root for frontend (browser), amplify/ for backend (CDK synthesis)