Page updated Feb 9, 2024

Preview: AWS Amplify's new code-first DX (Gen 2)

The next generation of Amplify's backend building experience with a TypeScript-first DX.

Get started

Customize your data model

You are currently viewing the new GraphQL transformer v2 docs Looking for legacy docs?

Amplify automatically creates Amazon DynamoDB database tables for GraphQL types annotated with the @model directive in your GraphQL schema. You can create relations between the data models via the @hasOne, @hasMany, @belongsTo, and @manyToMany directives.

Setup database tables

The following GraphQL schema automatically creates a database table for "Todo". @model will also automatically add an id field as a primary key to the database table. See Configure a primary key to learn how to customize the primary key.

type Todo @model { content: String }
1type Todo @model {
2 content: String
3}

Upon amplify push or cdk deploy, Amplify deploys the Todo database table and a corresponding GraphQL API to perform create, read, update, delete, and list operations.

In addition, @model also adds the helper fields createdAt and updatedAt to your type. The values for those fields are read-only by clients unless explicitly overwritten. See Customize creation and update timestamps to learn more.

Try listing all the todos by executing the following query:

query QueryAllTodos { listTodos() { todos { items { id content createdAt updatedAt } } } }
1query QueryAllTodos {
2 listTodos() {
3 todos {
4 items {
5 id
6 content
7 createdAt
8 updatedAt
9 }
10 }
11 }
12}
import { Amplify } from 'aws-amplify'; import { generateClient } from 'aws-amplify/api'; import config from './amplifyconfiguration.json'; import { listTodos } from './graphql/queries'; const client = generateClient(); Amplify.configure(config); try { const result = await client.graphql({ query: listTodos }); const todos = result.data.listTodos; } catch (res) { const { errors } = res; console.error(errors); }
1import { Amplify } from 'aws-amplify';
2import { generateClient } from 'aws-amplify/api';
3import config from './amplifyconfiguration.json';
4import { listTodos } from './graphql/queries';
5
6const client = generateClient();
7
8Amplify.configure(config);
9
10try {
11 const result = await client.graphql({ query: listTodos });
12 const todos = result.data.listTodos;
13} catch (res) {
14 const { errors } = res;
15 console.error(errors);
16}

Configure a primary key

Every GraphQL type with the @model directive will automatically have an id field set as the primary key. You can override this behavior by marking another required field with the @primaryKey directive.

In the example below, todoId is the database's primary key and an id field will no longer be created automatically anymore by the @model directive.

type Todo @model { todoId: ID! @primaryKey content: String }
1type Todo @model {
2 todoId: ID! @primaryKey
3 content: String
4}

Without any further configuration, you'll only be able to query for a Todo via an exact equality match of its primary key field. In the example above, this is the todoId field.

Note: After a primary key is configured and deployed, you can't change it without deleting and recreating your database table.

You can also specify "sort keys" to use a combination of different fields as a primary key. This also allows you to apply more advanced sorting and filtering conditions on the specified "sort key fields".

type Inventory @model { productID: ID! @primaryKey(sortKeyFields: ["warehouseID"]) warehouseID: ID! InventoryAmount: Int! }
1type Inventory @model {
2 productID: ID! @primaryKey(sortKeyFields: ["warehouseID"])
3 warehouseID: ID!
4 InventoryAmount: Int!
5}

The schema above will allow you to pass different conditions to query the correct inventory item:

query QueryInventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) { getInventory(productID: $productID, warehouseID: $warehouseID) { productID warehouseID inventoryAmount } }
1query QueryInventoryByProductAndWarehouse($productID: ID!, $warehouseID: ID!) {
2 getInventory(productID: $productID, warehouseID: $warehouseID) {
3 productID
4 warehouseID
5 inventoryAmount
6 }
7}
import { getInventory } from './graphql/queries'; const result = await client.graphql({ query: getInventory, variables: { productID: 'product-id', warehouseID: 'warehouse-id' } }); const inventory = result.data.getInventory;
1import { getInventory } from './graphql/queries';
2
3const result = await client.graphql({
4 query: getInventory,
5 variables: {
6 productID: 'product-id',
7 warehouseID: 'warehouse-id'
8 }
9});
10const inventory = result.data.getInventory;

Configure a secondary index

Amplify uses Amazon DynamoDB tables as the underlying data source for @model types. For key-value databases, it is critical to model your access patterns with "secondary indexes". Use the @index directive to configure a secondary index.

Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale but making it work for your access patterns requires a bit of forethought. DynamoDB query operations may use at most two attributes to efficiently query data. The first query argument passed to a query (the hash key) must use strict equality and the second attribute (the sort key) may use gt, ge, lt, le, eq, beginsWith, and between. DynamoDB can effectively implement a wide variety of access patterns that are powerful enough for the majority of applications.

A secondary index consists of a "hash key" and, optionally, a "sort key". Use the "hash key" to perform strict equality and the "sort key" for greater than (gt), greater than or equal to (ge), less than (lt), less than or equal to (le), equals (eq), begins with, and between operations.

type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index }
1type Customer @model {
2 id: ID!
3 name: String!
4 phoneNumber: String
5 accountRepresentativeID: ID! @index
6}

The example client query below allows you to query for "Customer" records based on their accountRepresentativeID:

query QueryCustomersForAccountRepresentative($accountRepresentativeID: ID!) { customersByAccountRepresentativeID( accountRepresentativeID: $accountRepresentativeID ) { customers { items { id name phoneNumber } } } }
1query QueryCustomersForAccountRepresentative($accountRepresentativeID: ID!) {
2 customersByAccountRepresentativeID(
3 accountRepresentativeID: $accountRepresentativeID
4 ) {
5 customers {
6 items {
7 id
8 name
9 phoneNumber
10 }
11 }
12 }
13}
import { customersByAccountRepresentativeID } from './graphql/queries'; const result = await client.graphql({ query: customersByAccountRepresentativeID, variables: { accountRepresentativeID: 'account-rep-id' } }); const customers = result.data.customersByAccountRepresentativeID;
1import { customersByAccountRepresentativeID } from './graphql/queries';
2
3const result = await client.graphql({
4 query: customersByAccountRepresentativeID,
5 variables: {
6 accountRepresentativeID: 'account-rep-id'
7 }
8});
9const customers = result.data.customersByAccountRepresentativeID;

You can also overwrite the queryField or name to customize the GraphQL query name, or secondary index name respectively:

type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index(name: "byRepresentative", queryField: "customerByRepresentative") }
1type Customer @model {
2 id: ID!
3 name: String!
4 phoneNumber: String
5 accountRepresentativeID: ID!
6 @index(name: "byRepresentative", queryField: "customerByRepresentative")
7}
query QueryCustomersForAccountRepresentative($representativeId: ID!) { customerByRepresentative(accountRepresentativeID: $representativeId) { customers { items { id name phoneNumber } } } }
1query QueryCustomersForAccountRepresentative($representativeId: ID!) {
2 customerByRepresentative(accountRepresentativeID: $representativeId) {
3 customers {
4 items {
5 id
6 name
7 phoneNumber
8 }
9 }
10 }
11}
import { customerByRepresentative } from './graphql/queries'; const result = await client.graphql({ query: customerByRepresentative, variables: { accountRepresentativeID: 'account-rep-id' } }); const customer = result.data.customerByRepresentative;
1import { customerByRepresentative } from './graphql/queries';
2
3const result = await client.graphql({
4 query: customerByRepresentative,
5 variables: {
6 accountRepresentativeID: 'account-rep-id'
7 }
8});
9const customer = result.data.customerByRepresentative;

To optionally configure sort keys, provide the additional fields in the sortKeyFields parameter:

type Customer @model @auth(rules: [{ allow: public }]) { id: ID! name: String! @index(name: "byNameAndPhoneNumber", sortKeyFields: ["phoneNumber"], queryField: "customerByNameAndPhone") phoneNumber: String accountRepresentativeID: ID! @index
1type Customer @model @auth(rules: [{ allow: public }]) {
2 id: ID!
3 name: String! @index(name: "byNameAndPhoneNumber", sortKeyFields: ["phoneNumber"], queryField: "customerByNameAndPhone")
4 phoneNumber: String
5 accountRepresentativeID: ID! @index

The example client query below allows you to query for "Customer" based on their name and filter based on phoneNumber:

query MyQuery { customerByNameAndPhone(phoneNumber: { beginsWith: "+1" }, name: "Rene") { items { id name phoneNumber } } }
1query MyQuery {
2 customerByNameAndPhone(phoneNumber: { beginsWith: "+1" }, name: "Rene") {
3 items {
4 id
5 name
6 phoneNumber
7 }
8 }
9}
import { customerByNameAndPhone } from './graphql/queries'; const result = await client.graphql({ query: customerByNameAndPhone, variables: { phoneNumber: { beginsWith: '+1' }, name: 'Rene' } }); const customer = result.data.customerByNameAndPhone;
1import { customerByNameAndPhone } from './graphql/queries';
2
3const result = await client.graphql({
4 query: customerByNameAndPhone,
5 variables: {
6 phoneNumber: { beginsWith: '+1' },
7 name: 'Rene'
8 }
9});
10
11const customer = result.data.customerByNameAndPhone;

Setup relationships between models

Create "has one", "has many", "belongs to", and "many to many" relationships between @model types.

RelationshipDescription
@hasOneCreate a one-directional one-to-one relationship between two models. For example, a Project "has one" Team. This allows you to query the team from the project record.
@hasManyCreate a one-directional one-to-many relationship between two models. For example, a Post has many comments. This allows you to query all the comments from the post record.
@belongsToUse a "belongs to" relationship to make a "has one" or "has many" relationship bi-directional. For example, a Project has one Team and a Team belongs to a Project. This allows you to query the team from the project record and vice versa.
@manyToManyConfigures a "join table" between two models to facilitate a many-to-many relationship. For example, a Blog has many Tags and a Tag has many Blogs.

Has One relationship

The @hasOne and @hasMany directives do not support referencing a model which then references the initial model via @hasOne or @hasMany if DataStore is enabled.

Create a one-directional one-to-one relationship between two models using the @hasOne directive.

In the example below, a Project has a Team.

type Project @model { id: ID! name: String team: Team @hasOne } type Team @model { id: ID! name: String! }
1type Project @model {
2 id: ID!
3 name: String
4 team: Team @hasOne
5}
6
7type Team @model {
8 id: ID!
9 name: String!
10}

This generates queries and mutations that allow you to retrieve the related record from the source record:

mutation CreateProject { createProject(input: { projectTeamId: "team-id", name: "Some Name" }) { team { name id } name id } }
1mutation CreateProject {
2 createProject(input: { projectTeamId: "team-id", name: "Some Name" }) {
3 team {
4 name
5 id
6 }
7 name
8 id
9 }
10}
import { createProject } from './graphql/mutations'; const result = await client.graphql({ query: createProject, variables: { input: { projectTeamId: 'team-id', name: 'Some Name' } } }); const project = result.data.createProject;
1import { createProject } from './graphql/mutations';
2
3const result = await client.graphql({
4 query: createProject,
5 variables: {
6 input: { projectTeamId: 'team-id', name: 'Some Name' }
7 }
8});
9
10const project = result.data.createProject;

To customize the field to be used for storing the relationship information, set the fields array argument and matching it to a field on the type:

type Project @model { id: ID! name: String teamID: ID team: Team @hasOne(fields: ["teamID"]) } type Team @model { id: ID! name: String! }
1type Project @model {
2 id: ID!
3 name: String
4 teamID: ID
5 team: Team @hasOne(fields: ["teamID"])
6}
7
8type Team @model {
9 id: ID!
10 name: String!
11}

In this case, the Project type has a teamID field added as an identifier for the team. @hasOne can then get the connected Team object by querying the Team table with this teamID:

mutation CreateProject { createProject(input: { name: "New Project", teamID: "a-team-id" }) { id name team { id name } } }
1mutation CreateProject {
2 createProject(input: { name: "New Project", teamID: "a-team-id" }) {
3 id
4 name
5 team {
6 id
7 name
8 }
9 }
10}
import { createProject } from './graphql/mutations'; const result = await client.graphql({ query: createProject, variables: { input: { teamID: 'team-id', name: 'New Project' } } }); const project = result.data.createProject;
1import { createProject } from './graphql/mutations';
2
3const result = await client.graphql({
4 query: createProject,
5 variables: {
6 input: {
7 teamID: 'team-id',
8 name: 'New Project'
9 }
10 }
11});
12const project = result.data.createProject;

A @hasOne relationship always uses a reference to the primary key of the related model, by default id unless overridden with the @primaryKey directive.

Has Many relationship

The @hasOne and @hasMany directives do not support referencing a model which then references the initial model via @hasOne or @hasMany if DataStore is enabled.

Create a one-directional one-to-many relationship between two models using the @hasMany directive.

type Post @model { id: ID! title: String! comments: [Comment] @hasMany } type Comment @model { id: ID! content: String! }
1type Post @model {
2 id: ID!
3 title: String!
4 comments: [Comment] @hasMany
5}
6
7type Comment @model {
8 id: ID!
9 content: String!
10}

This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record:

mutation CreatePost { createPost(input: { title: "Hello World!!" }) { title id comments { items { id content } } } }
1mutation CreatePost {
2 createPost(input: { title: "Hello World!!" }) {
3 title
4 id
5 comments {
6 items {
7 id
8 content
9 }
10 }
11 }
12}
import { createPost } from './graphql/mutations'; const result = await client.graphql({ query: createPost, variables { input: { title: 'Hello World!!' }, } }); const post = result.data.createPost; const comments = post.comments.items;
1import { createPost } from './graphql/mutations';
2
3const result = await client.graphql({
4 query: createPost,
5 variables {
6 input: { title: 'Hello World!!' },
7 }
8});
9const post = result.data.createPost;
10const comments = post.comments.items;

Under the hood, @hasMany configures a default secondary index on the related table to enable you to query the related model from the source model.

To customize the specific secondary index used for the "has many" relationship, create an @index directive on the field in the related table where you want to assign the secondary index.

Next, provide the secondary index with a name attribute and a value. Optionally, you can configure a “sort key” on the secondary index by providing a sortKeyFields attribute and a designated field as its value.

On the @hasMany configuration, pass in the name value from your secondary index as the value for the indexName parameter. Then, pass in the respective fields that match the connected index.

type Post @model { id: ID! title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"]) } type Comment @model { id: ID! postID: ID! @index(name: "byPost", sortKeyFields: ["content"]) content: String! }
1type Post @model {
2 id: ID!
3 title: String!
4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
5}
6
7type Comment @model {
8 id: ID!
9 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
10 content: String!
11}

In this case, the Comment type has a postID field added to store the reference of Post record. The id field referenced by @hasMany is the id on the Post type. @hasMany can then get the connected Comment object by querying the Comment table's secondary index "byPost" with this postID:

mutation CreatePost { createPost(input: { title: "Hello world!" }) { comments { items { postID content id } } title id } }
1mutation CreatePost {
2 createPost(input: { title: "Hello world!" }) {
3 comments {
4 items {
5 postID
6 content
7 id
8 }
9 }
10 title
11 id
12 }
13}
import { createPost, createComment } from './graphql/mutations'; import { getPost } from './graphql/mutations'; // create post const result = await client.graphql({ query: createPost, variables: { input: { title: 'Hello World!!' } } }); const post = result.data.createPost; // create comment await client.graphql({ query: createComment, variables: { input: { content: 'Hi!', postID: post.id } } }); // get post const result = await client.graphql({ query: getPost, variables: { id: post.id } }); const postWithComments = result.data.createPost; const postComments = postWithComments.comments.items; // access comments from post
1import { createPost, createComment } from './graphql/mutations';
2import { getPost } from './graphql/mutations';
3
4// create post
5const result = await client.graphql({
6 query: createPost,
7 variables: {
8 input: { title: 'Hello World!!' }
9 }
10});
11const post = result.data.createPost;
12
13// create comment
14await client.graphql({
15 query: createComment,
16 variables: {
17 input: { content: 'Hi!', postID: post.id }
18 }
19});
20
21// get post
22const result = await client.graphql({
23 query: getPost,
24 variables: { id: post.id }
25});
26
27const postWithComments = result.data.createPost;
28const postComments = postWithComments.comments.items; // access comments from post

Belongs To relationship

Make a "has one" or "has many" relationship bi-directional with the @belongsTo directive.

For 1:1 relationships, the @belongsTo directive solely facilitates the ability for you to query from both sides of the relationship. When updating a bi-directional hasOne relationship, you must update both sides of the relationship with corresponding IDs.

type Project @model { id: ID! name: String team: Team @hasOne } type Team @model { id: ID! name: String! project: Project @belongsTo }
1type Project @model {
2 id: ID!
3 name: String
4 team: Team @hasOne
5}
6
7type Team @model {
8 id: ID!
9 name: String!
10 project: Project @belongsTo
11}

This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record and vice versa:

mutation CreateProject { createProject(input: { name: "New Project", teamID: "a-team-id" }) { id name team { # query team from project id name project { # bi-directional query: team to project id name } } } }
1mutation CreateProject {
2 createProject(input: { name: "New Project", teamID: "a-team-id" }) {
3 id
4 name
5 team {
6 # query team from project
7 id
8 name
9 project {
10 # bi-directional query: team to project
11 id
12 name
13 }
14 }
15 }
16}
import { createProject, createTeam, updateTeam } from './graphql/mutations'; // create team const result = await client.graphql({ query: createTeam, variables: { input: { name: 'New Team' } } }); const team = result.data.createTeam; // create project const result = await client.graphql({ query: createProject, variables: { input: { name: 'New Project', projectTeamId: team.id } } }); const project = result.data.createProject; const projectTeam = project.team; // access team from project // update team const updateTeamResult = await client.graphql({ query: updateTeam, variables: { input: { id: team.id, teamProjectId: project.id } } }); const updatedTeam = updateTeamResult.data.updateTeam; const teamProject = updatedTeam.project; // access project from team
1import { createProject, createTeam, updateTeam } from './graphql/mutations';
2
3// create team
4const result = await client.graphql({
5 query: createTeam,
6 variables: {
7 input: { name: 'New Team' }
8 }
9});
10const team = result.data.createTeam;
11
12// create project
13const result = await client.graphql({
14 query: createProject,
15 variables: {
16 input: { name: 'New Project', projectTeamId: team.id }
17 }
18});
19const project = result.data.createProject;
20const projectTeam = project.team; // access team from project
21
22// update team
23const updateTeamResult = await client.graphql({
24 query: updateTeam,
25 variables: {
26 input: { id: team.id, teamProjectId: project.id }
27 }
28});
29
30const updatedTeam = updateTeamResult.data.updateTeam;
31const teamProject = updatedTeam.project; // access project from team
type Post @model { id: ID! title: String! comments: [Comment] @hasMany } type Comment @model { id: ID! content: String! post: Post @belongsTo }
1type Post @model {
2 id: ID!
3 title: String!
4 comments: [Comment] @hasMany
5}
6
7type Comment @model {
8 id: ID!
9 content: String!
10 post: Post @belongsTo
11}

This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record and vice versa:

mutation CreatePost { createPost(input: { title: "Hello World!!" }) { title id comments { # query comments from the post items { id content post { # bi-directional query: comment to post id title } } } } }
1mutation CreatePost {
2 createPost(input: { title: "Hello World!!" }) {
3 title
4 id
5 comments {
6 # query comments from the post
7 items {
8 id
9 content
10 post {
11 # bi-directional query: comment to post
12 id
13 title
14 }
15 }
16 }
17 }
18}
import { createPost, createComment } from './graphql/mutations'; import { getPost } from './graphql/mutations'; // create post const result = await client.graphql({ query: createPost, variables: { input: { title: 'Hello World!!' } } }); const post = result.data.createPost; // create comment await client.graphql({ query: createComment, variables: { input: { content: 'Hi!', postID: post.id } } }); // get post const result = await client.graphql({ query: getPost, variables: { id: post.id } }); const postWithComments = result.data.createPost; const postComments = postWithComments.comments.items; // access comments from post const commentPost = postComments[0].post; // access post from comment;
1import { createPost, createComment } from './graphql/mutations';
2import { getPost } from './graphql/mutations';
3
4// create post
5const result = await client.graphql({
6 query: createPost,
7 variables: {
8 input: { title: 'Hello World!!' }
9 }
10});
11const post = result.data.createPost;
12
13// create comment
14await client.graphql({
15 query: createComment,
16 variables: {
17 input: { content: 'Hi!', postID: post.id }
18 }
19});
20
21// get post
22const result = await client.graphql({
23 query: getPost,
24 variables: { id: post.id }
25});
26
27const postWithComments = result.data.createPost;
28const postComments = postWithComments.comments.items; // access comments from post
29
30const commentPost = postComments[0].post; // access post from comment;

@belongsTo can be used without the fields argument. In those cases, a field is automatically generated to reference the parent’s primary key.

Alternatively, you set up a custom field to store the reference of the parent object. An example bidirectional “has many” relationship is shown below.

type Post @model { id: ID! title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"]) } type Comment @model { id: ID! postID: ID! @index(name: "byPost", sortKeyFields: ["content"]) content: String! post: Post @belongsTo(fields: ["postID"]) }
1type Post @model {
2 id: ID!
3 title: String!
4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
5}
6
7type Comment @model {
8 id: ID!
9 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
10 content: String!
11 post: Post @belongsTo(fields: ["postID"])
12}

Note: The @belongsTo directive requires that a @hasOne or @hasMany relationship already exists from parent to the related model.

Many-to-many relationship

Create a many-to-many relationship between two models with the @manyToMany directive. Provide a common relationName on both models to join them into a many-to-many relationship.

type Post @model { id: ID! title: String! content: String tags: [Tag] @manyToMany(relationName: "PostTags") } type Tag @model { id: ID! label: String! posts: [Post] @manyToMany(relationName: "PostTags") }
1type Post @model {
2 id: ID!
3 title: String!
4 content: String
5 tags: [Tag] @manyToMany(relationName: "PostTags")
6}
7
8type Tag @model {
9 id: ID!
10 label: String!
11 posts: [Post] @manyToMany(relationName: "PostTags")
12}

Under the hood, the @manyToMany directive will create a "join table" named after the relationName to facilitate the many-to-many relationship. This generates queries and mutations that allow you to retrieve the related Comment records from the source Post record and vice versa:

mutation CreatePost { createPost(input: { title: "Hello World!!" }) { id title content tags { # queries the "join table" PostTags items { tag { # related Tag records from Post id label posts { # queries the "join table" PostTags items { post { # related Post records from Tag id title content } } } } } } } }
1mutation CreatePost {
2 createPost(input: { title: "Hello World!!" }) {
3 id
4 title
5 content
6 tags {
7 # queries the "join table" PostTags
8 items {
9 tag {
10 # related Tag records from Post
11 id
12 label
13 posts {
14 # queries the "join table" PostTags
15 items {
16 post {
17 # related Post records from Tag
18 id
19 title
20 content
21 }
22 }
23 }
24 }
25 }
26 }
27 }
28}
import { createPost, createTag, createPostTags } from './graphql/mutations'; import { listPosts } from './graphql/queries'; // create post const result = await client.graphql({ query: createPost, variables: { input: { title: 'Hello World' } } }); const post = result.data.createPost; // create tag const tagResult = await client.graphql({ query: createTag, variables: { input: { label: 'My Tag' } } }); const tag = tagResult.data.createTag; // connect post and tag await client.graphql({ query: createPostTags, variables: { input: { postId: post.id, tagId: tag.id } } }); // get posts const listPostsResult = await client.graphql({ query: listPosts }); const posts = listPostsResult.data.listPosts; const postTags = posts[0].tags; // access tags from post
1import { createPost, createTag, createPostTags } from './graphql/mutations';
2import { listPosts } from './graphql/queries';
3
4// create post
5const result = await client.graphql({
6 query: createPost,
7 variables: {
8 input: { title: 'Hello World' }
9 }
10});
11const post = result.data.createPost;
12
13// create tag
14const tagResult = await client.graphql({
15 query: createTag,
16 variables: {
17 input: {
18 label: 'My Tag'
19 }
20 }
21});
22const tag = tagResult.data.createTag;
23
24// connect post and tag
25await client.graphql({
26 query: createPostTags,
27 variables: {
28 input: {
29 postId: post.id,
30 tagId: tag.id
31 }
32 }
33});
34
35// get posts
36const listPostsResult = await client.graphql({ query: listPosts });
37const posts = listPostsResult.data.listPosts;
38
39const postTags = posts[0].tags; // access tags from post

Assign default values for fields

You can use the @default directive to specify a default value for optional scalar type fields such as Int, String, and more.

type Todo @model { content: String @default(value: "My new Todo") # Note: all "value" parameters must be passed as a string value. # Under the hood, Amplify will parse the string values into respective types. # For example, to set a default value for an integer field, # you must pass in `"0"` instead of `0` without the double-quotes. likes: Int @default(value: "0") # }
1type Todo @model {
2 content: String @default(value: "My new Todo")
3 # Note: all "value" parameters must be passed as a string value.
4 # Under the hood, Amplify will parse the string values into respective types.
5 # For example, to set a default value for an integer field,
6 # you must pass in `"0"` instead of `0` without the double-quotes.
7 likes: Int @default(value: "0") #
8}

If you create a new Todo and don't supply a content input, Amplify will ensure that My new Todo is auto populated as a value. When @default is applied, non-null assertions using ! are disregarded. For example, String! is treated the same as String.

Server-side filtering for subscriptions

A server-side subscription filter expression is automatically generated for any @model type.

type Task @model { title: String! description: String type: String priority: Int }
1type Task @model {
2 title: String!
3 description: String
4 type: String
5 priority: Int
6}

You can filter the subscriptions server-side by passing a filter expression. For example: If you want to subscribe to tasks of type Security and priority greater than 5, you can set the filter argument accordingly.

subscription OnCreateTask { onCreateTask( filter: { and: [{ type: { eq: "Security" } }, { priority: { gt: 5 } }] } ) { title description type priority } }
1subscription OnCreateTask {
2 onCreateTask(
3 filter: { and: [{ type: { eq: "Security" } }, { priority: { gt: 5 } }] }
4 ) {
5 title
6 description
7 type
8 priority
9 }
10}
import { onCreateTask } from './graphql/subscriptions'; const subscription = client.graphql({ query: onCreateTask, variables: { filter: { and: [ { type: { eq: "Security" } } { priority: { gt: 5 } } ] } } }).subscribe({ next: ({ data }) => console.log(data), error: (error) => console.warn(error) });
1import { onCreateTask } from './graphql/subscriptions';
2
3const subscription = client.graphql({
4 query: onCreateTask,
5 variables: {
6 filter: {
7 and: [
8 { type: { eq: "Security" } }
9 { priority: { gt: 5 } }
10 ]
11 }
12 }
13}).subscribe({
14 next: ({ data }) => console.log(data),
15 error: (error) => console.warn(error)
16});

If you want to get all subscription events, don’t pass any filter parameters.

Important: Passing an empty object {} as a filter is NOT recommended. Using {} as a filter might cause inconsistent behavior based on your data model's authorization rules.

Advanced

Rename generated queries, mutations, and subscriptions

You can override the names of any @model-generated GraphQL queries, mutations, and subscriptions by supplying the desired name.

type Todo @model(queries: { get: "queryFor" }) { name: String! description: String }
1type Todo @model(queries: { get: "queryFor" }) {
2 name: String!
3 description: String
4}

In the example above, you will be able to run a queryForTodo query to get a single Todo element.

Disable generated queries, mutations, and subscriptions

You can disable specific operations by assigning their value to null.

type Todo @model(queries: { get: null }, mutations: null, subscriptions: null) { name: String! description: String }
1type Todo @model(queries: { get: null }, mutations: null, subscriptions: null) {
2 name: String!
3 description: String
4}

The example above disables the getTodo query, all mutations, and all subscriptions while allowing the generation of other queries such as listTodo.

Creating a custom query

You can disable the get query and create a custom query that enables us to retrieve a single Todo model.

type Query { getMyTodo(id: ID!): Todo @function(name: "getmytodofunction-${env}") }
1type Query {
2 getMyTodo(id: ID!): Todo @function(name: "getmytodofunction-${env}")
3}

The example above creates a custom query that utilizes the @function directive to call a Lambda function for this query.

For the type definitions of queries, mutations, and subscriptions, see Type Definitions of the @model Directive.

Customize creation and update timestamps

The @model directive automatically adds createdAt and updatedAt timestamps to each entity. The timestamp field names can be changed by passing timestamps attribute to the directive.

type Todo @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) { name: String! description: String }
1type Todo
2 @model(timestamps: { createdAt: "createdOn", updatedAt: "updatedOn" }) {
3 name: String!
4 description: String
5}

For example, the schema above will allow you to query for the following contents:

type Todo { id: ID! name: String! description: String createdOn: AWSDateTime! updatedOn: AWSDateTime! }
1type Todo {
2 id: ID!
3 name: String!
4 description: String
5 createdOn: AWSDateTime!
6 updatedOn: AWSDateTime!
7}

Modify subscriptions (real-time updates) access level

By default, real-time updates are on for all @model types, which means customers receive real-time updates and authorization rules are applied during initial connection time. You can also turn off subscriptions for that model or make the real-time updates public, receivable by all subscribers.

type Todo @model(subscriptions: { level: off }) { # or level: public name: String! description: String }
1type Todo
2 @model(subscriptions: { level: off }) { # or level: public
3 name: String!
4 description: String
5}

Create multiple relationships between two models

You need to explicitly specify the connection field names if relational directives are used to create two connections of the same type between the two models.

type Individual @model { id: ID! homeAddress: Address @hasOne shippingAddress: Address @hasOne } type Address @model { id: ID! homeIndividualID: ID shippingIndividualID: ID homeIndividual: Individual @belongsTo(fields: ["homeIndividualID"]) shipIndividual: Individual @belongsTo(fields: ["shippingIndividualID"]) }
1type Individual @model {
2 id: ID!
3 homeAddress: Address @hasOne
4 shippingAddress: Address @hasOne
5}
6
7type Address @model {
8 id: ID!
9 homeIndividualID: ID
10 shippingIndividualID: ID
11 homeIndividual: Individual @belongsTo(fields: ["homeIndividualID"])
12 shipIndividual: Individual @belongsTo(fields: ["shippingIndividualID"])
13}

Relationships to a model with a composite primary key

When a primary key is defined by a sort key in addition to the hash key, then it's called a composite primary key.

If you explicitly define the fields argument on the @hasOne, @hasMany, or @belongsTo directives and reference a model that has a composite primary key, then you must set the values in the fields argument in a specific order:

  • The first value should always be the primary key of the related model.
  • Remaining values should match the sortKeyFields specified in the @primaryKey directive of the related model.
type Project @model { projectId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! team: Team @hasOne(fields: ["teamId", "teamName"]) teamId: ID # customized foreign key for child primary key teamName: String # customized foreign key for child sort key } type Team @model { teamId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! }
1type Project @model {
2 projectId: ID! @primaryKey(sortKeyFields: ["name"])
3 name: String!
4 team: Team @hasOne(fields: ["teamId", "teamName"])
5 teamId: ID # customized foreign key for child primary key
6 teamName: String # customized foreign key for child sort key
7}
8
9type Team @model {
10 teamId: ID! @primaryKey(sortKeyFields: ["name"])
11 name: String!
12}
type Project @model { projectId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! team: Team @hasOne(fields: ["teamId", "teamName"]) teamId: ID # customized foreign key for child primary key teamName: String # customized foreign key for child sort key } type Team @model { teamId: ID! @primaryKey(sortKeyFields: ["name"]) name: String! project: Project @belongsTo(fields: ["projectId", "projectName"]) projectId: ID # customized foreign key for parent primary key projectName: String # customized foreign key for parent sort key }
1type Project @model {
2 projectId: ID! @primaryKey(sortKeyFields: ["name"])
3 name: String!
4 team: Team @hasOne(fields: ["teamId", "teamName"])
5 teamId: ID # customized foreign key for child primary key
6 teamName: String # customized foreign key for child sort key
7}
8
9type Team @model {
10 teamId: ID! @primaryKey(sortKeyFields: ["name"])
11 name: String!
12 project: Project @belongsTo(fields: ["projectId", "projectName"])
13 projectId: ID # customized foreign key for parent primary key
14 projectName: String # customized foreign key for parent sort key
15}
type Post @model { postId: ID! @primaryKey(sortKeyFields: ["title"]) title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"]) } type Comment @model { commentId: ID! @primaryKey(sortKeyFields: ["content"]) content: String! postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key postTitle: String # customized foreign key for parent sort key }
1type Post @model {
2 postId: ID! @primaryKey(sortKeyFields: ["title"])
3 title: String!
4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"])
5}
6
7type Comment @model {
8 commentId: ID! @primaryKey(sortKeyFields: ["content"])
9 content: String!
10 postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key
11 postTitle: String # customized foreign key for parent sort key
12}
type Post @model { postId: ID! @primaryKey(sortKeyFields: ["title"]) title: String! comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"]) } type Comment @model { commentId: ID! @primaryKey(sortKeyFields: ["content"]) content: String! post: Post @belongsTo(fields: ["postId", "postTitle"]) postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key postTitle: String # customized foreign key for parent sort key }
1type Post @model {
2 postId: ID! @primaryKey(sortKeyFields: ["title"])
3 title: String!
4 comments: [Comment] @hasMany(indexName: "byPost", fields: ["postId", "title"])
5}
6
7type Comment @model {
8 commentId: ID! @primaryKey(sortKeyFields: ["content"])
9 content: String!
10 post: Post @belongsTo(fields: ["postId", "postTitle"])
11 postId: ID @index(name: "byPost", sortKeyFields: ["postTitle"]) # customized foreign key for parent primary key
12 postTitle: String # customized foreign key for parent sort key
13}

Generate a secondary index without a GraphQL query

Because query creation against a secondary index is automatic, if you wish to define a secondary index that does not have a corresponding query in your API, set the queryField parameter to null.

type Customer @model { id: ID! name: String! phoneNumber: String accountRepresentativeID: ID! @index(queryField: null) }
1type Customer @model {
2 id: ID!
3 name: String!
4 phoneNumber: String
5 accountRepresentativeID: ID! @index(queryField: null)
6}

Split GraphQL files

Amplify Studio does not support splitting GraphQL schemas.

If using Amplify Studio, please follow the Limitations section of the Data Modeling documentation for Amplify Studio.

AWS Amplify supports splitting your GraphQL schema into separate .graphql files.

You can start by creating a amplify/backend/api/<api-name>/schema/ directory. As an example, you might split up the schema for a blog site by creating Blog.graphql, Post.graphql, and Comment.graphql files.

You can then run amplify api gql-compile and the output build schema will include all the types declared across your schema files.

As your project grows, you may want to organize your custom queries, mutations, and subscriptions depending on the size and maintenance requirements of your project. You can either consolidate all of them into one file or colocate them with their corresponding models.

Using a Single Query.graphql File

This method involves consolidating all queries into a single Query.graphql file. It is useful for smaller projects or when you want to keep all queries in one place.

  1. In the amplify/backend/api/<api-name>/schema/ directory, create a file named Query.graphql.

  2. Copy all query type definitions from your multiple schema files into the Query.graphql file.

  3. Make sure all your queries are properly formatted and enclosed within a single type Query { ... } block.

Using the extend Keyword

Declaring a Query type in separate schema files will result in schema validation errors similar to the following when running amplify api gql-compile:

🛑 Schema validation failed. There can be only one type named "Query".
1🛑 Schema validation failed.
2
3There can be only one type named "Query".

Amplify GraphQL schemas support the extend keyword, which allows you to extend types with additional fields. In this case, it also allows you to split your custom queries, mutations, and subscriptions into multiple files. This may be more ideal for larger, more complex projects.

  1. Organize your GraphQL schema into multiple files as per your project's architecture.

  2. In one of the files (e.g., schema1.graphql), declare your type normally:

type Query { # initial custom queries }
1type Query {
2 # initial custom queries
3}
  1. In other schema files (e.g., schema2.graphql), use the extend keyword to add to the type:
extend type Query { # additional custom queries }
1extend type Query {
2 # additional custom queries
3}

The order in which the Query types are extended does not affect the compilation of separate schema files.

Declaring custom Query, Mutation, and/or Subscription with the same field names in another schema file will result in schema validation errors similar to the following:

🛑 Object type extension 'Query' cannot redeclare field getBlogById

How it works

Model directive

The @model directive will generate:

  • An Amazon DynamoDB table with PAY_PER_REQUEST billing mode enabled by default.
  • An AWS AppSync DataSource configured to access the table above.
  • An AWS IAM role attached to the DataSource that allows AWS AppSync to call the above table on your behalf.
  • Up to 8 resolvers (create, update, delete, get, list, onCreate, onUpdate, onDelete) but this is configurable via the queries, mutations, and subscriptions arguments on the @model directive.
  • Input objects for create, update, and delete mutations.
  • Filter input objects that allow you to filter objects in list queries and relationship fields.
  • For list queries the default number of objects returned is 100. You can override this behavior by setting the limit argument.

Type definition of the @model directive

directive @model( queries: ModelQueryMap mutations: ModelMutationMap subscriptions: ModelSubscriptionMap timestamps: TimestampConfiguration ) on OBJECT input ModelMutationMap { create: String update: String delete: String } input ModelQueryMap { get: String list: String } input ModelSubscriptionMap { onCreate: [String] onUpdate: [String] onDelete: [String] level: ModelSubscriptionLevel } enum ModelSubscriptionLevel { off public on } input TimestampConfiguration { createdAt: String updatedAt: String }
1directive @model(
2 queries: ModelQueryMap
3 mutations: ModelMutationMap
4 subscriptions: ModelSubscriptionMap
5 timestamps: TimestampConfiguration
6) on OBJECT
7
8input ModelMutationMap {
9 create: String
10 update: String
11 delete: String
12}
13
14input ModelQueryMap {
15 get: String
16 list: String
17}
18
19input ModelSubscriptionMap {
20 onCreate: [String]
21 onUpdate: [String]
22 onDelete: [String]
23 level: ModelSubscriptionLevel
24}
25
26enum ModelSubscriptionLevel {
27 off
28 public
29 on
30}
31
32input TimestampConfiguration {
33 createdAt: String
34 updatedAt: String
35}

Relational directives

The relational directives are @hasOne, @hasMany, @belongsTo and @manyToMany.

The @hasOne will generate:

  • Foreign key fields in parent type that refer to the primary key and sort key fields of the child model.
  • Foreign key fields in parent input object of create and update mutations.

Type definition of the @hasOne directive

directive @hasOne(fields: [String!]) on FIELD_DEFINITION
1directive @hasOne(fields: [String!]) on FIELD_DEFINITION

The @hasMany will generate:

  • Foreign key fields in child type that refer to the primary key and sort key fields of the parent model.
  • Foreign key fields in child input object of create and update mutations.
  • A global secondary index (GSI) in the child type Amazon DynamoDB table.

Type definition of the @hasMany directive

directive @hasMany( indexName: String fields: [String!] limit: Int = 100 ) on FIELD_DEFINITION
1directive @hasMany(
2 indexName: String
3 fields: [String!]
4 limit: Int = 100
5) on FIELD_DEFINITION
  • The default number of nested objects returned is 100. You can override this behavior by setting the limit argument.

The @belongsTo will generate:

  • Foreign key fields that refer to the primary key and sort key fields of the related model.
  • Foreign key fields in the input object of create and update mutations.

Type definition of the @belongsTo directive

directive @belongsTo(fields: [String!]) on FIELD_DEFINITION
1directive @belongsTo(fields: [String!]) on FIELD_DEFINITION

The @manyToMany will generate:

  • A joint table defining the intermediate model type with the name of relationName.
  • Foreign key fields in the joint table that refer to the primary key and sort key fields of both models.
  • Foreign key fields in the intermediate model input object of create and update mutations.

Type definition of the @manyToMany directive

directive @manyToMany( relationName: String! limit: Int = 100 ) on FIELD_DEFINITION
1directive @manyToMany(
2 relationName: String!
3 limit: Int = 100
4) on FIELD_DEFINITION
  • The default number of nested objects returned is 100. You can override this behavior by setting the limit argument.