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) |
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()])
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