Defining Data Models

In Amplify Gen 2, you define your data models using TypeScript. These models become DynamoDB tables, GraphQL types, and TypeScript interfaces.

The Data Resource File

Data models are defined in amplify/data/resource.ts. Let's start with a simple Todo model:

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

const schema = a.schema({
  Todo: a.model({
    content: a.string().required(),
    completed: a.boolean().default(false),
  }),
});

export type Schema = ClientSchema<typeof schema>;

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

Understanding the Syntax

Schema Definition

const schema = a.schema({
  // Models go here
});

The a.schema() function creates a schema containing all your models. Think of this as your database structure.

Model Definition

Todo: a.model({
  content: a.string().required(),
  completed: a.boolean().default(false),
})

a.model() creates a data model. Each key-value pair inside represents a field and its type.

Field Types

Amplify provides several field types:

Type Description Example
a.string() Text string name: a.string()
a.integer() Whole number count: a.integer()
a.float() Decimal number price: a.float()
a.boolean() True/false active: a.boolean()
a.datetime() ISO 8601 datetime dueDate: a.datetime()
a.date() Date only (no time) birthDate: a.date()
a.time() Time only (no date) startTime: a.time()
a.json() Arbitrary JSON metadata: a.json()
a.id() Unique identifier userId: a.id()
a.enum() Predefined values status: a.enum(['PENDING', 'DONE'])

Field Modifiers

Required Fields

content: a.string().required()

By default, all fields are optional. Use .required() to make a field mandatory.

Default Values

completed: a.boolean().default(false)
createdAt: a.datetime().default()

Set default values with .default(value). For datetime, .default() without a value sets the current timestamp.

Arrays

tags: a.string().array()

Use .array() to store multiple values of the same type.

Complete Todo Example

Let's create a more complete Todo model:

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

const schema = a.schema({
  Todo: a
    .model({
      content: a.string().required(),
      description: a.string(),
      completed: a.boolean().default(false),
      priority: a.enum(['LOW', 'MEDIUM', 'HIGH']),
      dueDate: a.datetime(),
      tags: a.string().array(),
    })
    .authorization((allow) => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

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

Auto-Generated Fields

Amplify automatically adds these fields to every model:

Field Type Description
id ID (UUID) Unique identifier, auto-generated
createdAt AWSDateTime Timestamp when item was created
updatedAt AWSDateTime Timestamp when item was last updated
owner String User ID of owner (when using owner auth)
💡
No Need to Define ID

You don't need to add an id field—Amplify handles this automatically. Every item gets a unique UUID.

Relationships

Models can have relationships with other models. Here's an example with Categories and Todos:

const schema = a.schema({
  Category: a
    .model({
      name: a.string().required(),
      color: a.string(),
      todos: a.hasMany('Todo', 'categoryId'),
    })
    .authorization((allow) => [allow.owner()]),

  Todo: a
    .model({
      content: a.string().required(),
      completed: a.boolean().default(false),
      categoryId: a.id(),
      category: a.belongsTo('Category', 'categoryId'),
    })
    .authorization((allow) => [allow.owner()]),
});

Relationship Types

Type Usage Description
hasOne a.hasOne('Model', 'fieldId') One-to-one relationship
hasMany a.hasMany('Model', 'fieldId') One-to-many relationship
belongsTo a.belongsTo('Model', 'fieldId') Many-to-one (inverse of hasMany)

Custom Identifiers

By default, Amplify uses a UUID for the id field. You can customize the identifier:

User: a
  .model({
    email: a.string().required(),
    name: a.string(),
  })
  .identifier(['email'])  // Use email as primary key
  .authorization((allow) => [allow.owner()])
⚠️
Immutable Identifiers

Once set, the identifier cannot be changed. Choose carefully—if using email as an identifier, users won't be able to change their email.

Enums

Use enums for fields with a fixed set of values:

// Define enum inline
priority: a.enum(['LOW', 'MEDIUM', 'HIGH'])

// Or define separately for reuse
const Priority = a.enum(['LOW', 'MEDIUM', 'HIGH']);
const Status = a.enum(['PENDING', 'IN_PROGRESS', 'COMPLETED']);

const schema = a.schema({
  Todo: a.model({
    content: a.string().required(),
    priority: Priority,
    status: Status,
  }),
});

Deploying Schema Changes

After modifying your schema, the sandbox will automatically detect changes and redeploy. Watch the terminal for:

✔ Deployed data/resource.ts
  Table: Todo-xxxx
  GraphQL API: https://xxxxx.appsync-api.us-east-1.amazonaws.com/graphql

Generate Client Types

After schema changes, regenerate the TypeScript types for your frontend:

npx ampx generate graphql-client-code

This creates type-safe client code that matches your schema exactly.

Summary

  • Data models are defined in amplify/data/resource.ts
  • Use a.model() to create models with typed fields
  • Field types include string, number, boolean, datetime, and more
  • Use modifiers like .required() and .default()
  • Relationships connect models with hasMany, belongsTo, etc.
  • Amplify auto-generates id, createdAt, and updatedAt fields