Introduction#

GitHub this note shows how to build a next app with aws appsync

  • Setup schema.graphql
  • Setup appsync api and dynamodb tables, and resolvers
  • Build next app

Appsync API#

First let create a schema.graphql

type Book {
id: ID!
title: String
author: String
description: String
order: Int
url: String
amazon: String
image: String
}
type Message {
id: ID!
content: String
}
type PaginatedMessages {
messages: [Message]
nextToken: String
}
type Schema {
query: Query
mutation: Mutation
}
type Mutation {
addMessage(id: ID, content: String): Message
addBook(
id: ID
title: String
author: String
description: String
order: Int
url: String
amazon: String
image: String
): Book
}
type Query {
getBook(id: ID!): Book
listBooks: [Book]
getMessage(id: ID!): Message
listMessages(limit: Int, nextToken: String): PaginatedMessages
}

Second, let create an appsync api stack

import { Stack, StackProps, aws_appsync, aws_iam } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as path from "path";
import * as fs from "fs";
export class AppsyncStack extends Stack {
public readonly apiId: string;
public readonly roleArn: string;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
const appsyncApi = new aws_appsync.CfnGraphQLApi(this, "nextappsync", {
name: "nextappsync",
authenticationType: "API_KEY",
});
new aws_appsync.CfnApiKey(this, "ItemsApiKey", {
apiId: appsyncApi.attrApiId,
});
new aws_appsync.CfnGraphQLSchema(this, "ItemsSchema", {
apiId: appsyncApi.attrApiId,
definition: fs.readFileSync(
path.resolve(__dirname, "./../config/schema.graphql"),
{ encoding: "utf-8" }
),
});
const role = new aws_iam.Role(this, "RoleForAppSyncAccessDDB", {
assumedBy: new aws_iam.ServicePrincipal("appsync.amazonaws.com"),
roleName: "RoleForAppSyncAccessDDB",
});
role.addManagedPolicy(
aws_iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess")
);
this.roleArn = role.roleArn;
this.apiId = appsyncApi.attrApiId;
}
}

Third, create a book table and resolver

import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { aws_dynamodb, RemovalPolicy, aws_appsync } from "aws-cdk-lib";
interface BookTableProps extends StackProps {
apiId: string;
roleArn: string;
}
export class BookTableStack extends Stack {
constructor(scope: Construct, id: string, props: BookTableProps) {
super(scope, id, props);
const bookTable = new aws_dynamodb.Table(this, "BookTable", {
tableName: "Book",
partitionKey: {
name: "id",
type: aws_dynamodb.AttributeType.STRING,
},
removalPolicy: RemovalPolicy.DESTROY,
});
const bookDataSource = new aws_appsync.CfnDataSource(
this,
"BookDataSource",
{
apiId: props.apiId,
name: "BookDataSource",
type: "AMAZON_DYNAMODB",
dynamoDbConfig: {
tableName: bookTable.tableName,
awsRegion: this.region,
},
serviceRoleArn: props.roleArn,
}
);
const getBookResolver = new aws_appsync.CfnResolver(
this,
"GetBookResolver",
{
apiId: props.apiId,
typeName: "Query",
fieldName: "getBook",
dataSourceName: bookDataSource.name,
requestMappingTemplate: `{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
}
}`,
responseMappingTemplate: `$util.toJson($ctx.result)`,
}
);
const listBooksResolver = new aws_appsync.CfnResolver(
this,
"ListBooksResolver",
{
apiId: props.apiId,
typeName: "Query",
fieldName: "listBooks",
dataSourceName: bookDataSource.name,
requestMappingTemplate: `{
"version": "2017-02-28",
"operation": "Scan"
}`,
responseMappingTemplate: `$util.toJson($ctx.result.items)`,
}
);
const addBookResolver = new aws_appsync.CfnResolver(this, "AddBook", {
apiId: props.apiId,
typeName: "Mutation",
fieldName: "addBook",
dataSourceName: bookDataSource.name,
runtime: {
name: "APPSYNC_JS",
runtimeVersion: "1.0.0",
},
code: `
import { util } from '@aws-appsync/utils'
import * as ddb from '@aws-appsync/utils/dynamodb'
export function request(ctx) {
const item = { ...ctx.arguments }
const key = { id: ctx.args.id ?? util.autoId() }
return ddb.put({ key, item })
}
export function response(ctx) {
return ctx.result
}`,
});
getBookResolver.addDependency(bookDataSource);
listBooksResolver.addDependency(bookDataSource);
addBookResolver.addDependency(bookDataSource);
}
}

Client Setup#

Then generate queries command typescript

npm install -g @aws-amplify/cli
amplify add codegen --apiId tyweijjzgvfqjeqqat52natp4m
amplify codegen

Create configure for Amplify in a file appsync.ts

export const config = {
aws_appsync_graphqlEndpoint: process.env.aws_appsync_graphqlEndpoint,
aws_appsync_region: process.env.aws_appsync_region,
aws_appsync_authenticationType: process.env.aws_appsync_authenticationType,
aws_appsync_apiKey: process.env.aws_appsync_apiKey,
};
export type Book = {
id: string;
title: string;
author: string;
description: string;
order: Number;
url: string;
amazon: string;
image: string;
};

List books from book table

import { listBooks } from "@/src/graphql/queries";
import { API } from "@aws-amplify/api";
import { config } from "./appsync";
import { Book } from "./appsync";
import Image from "next/image";
API.configure(config);
const getBooks = async () => {
"use server";
const response = (await API.graphql({
query: listBooks,
})) as any;
const books = response.data.listBooks as [Book];
console.log(books);
return books;
};
const Home = async () => {
let books: any[] = [];
try {
books = await getBooks();
} catch (error) {
console.log(error);
}
return (
<div className="min-h-screen dark:bg-slate-800">
<div className="mx-auto max-w-3xl px-5 dark:bg-slate-800 dark:text-white">
<div className="grid grid-cols-1 gap-5 pt-20">
{books.map((book, id) => (
<div
key={id}
className="dark:bg-slate-700 p-5 shadow-lg rounded-sm"
>
<h4 className="font-bold mb-8">{book.title}</h4>
<div>
<Image
src={book.image}
alt={book.title}
width={300}
height={300}
className="float-left h-auto w-64 mr-6"
loading="eager"
priority
></Image>
</div>
<p className="text-sm">{book.description}</p>
<a href={book.url} target="_blank">
<button className="bg-orange-400 px-14 py-3 rounded-sm shadow-md hover:bg-orange-300 mt-2">
Amazon
</button>
</a>
</div>
))}
</div>
</div>
</div>
);
};
export default Home;

Create a form to submit a book using a server action in action.ts as the following

"use server";
import { redirect } from "next/navigation";
import { addBook } from "@/src/graphql/mutations";
import { API } from "@aws-amplify/api";
import { config } from "../appsync";
API.configure(config);
const handleForm = async (data: FormData) => {
const title = data.get("title") ? data.get("title") : "Machine Learning";
const author = data.get("author") ? data.get("author") : "hai tran";
const url = data.get("url")
? data.get("url")
: "https://d2cvlmmg8c0xrp.cloudfront.net/book/data_engineering_with_aws.pdf";
const description = data.get("description")
? data.get("description")
: "no description";
const order = data.get("order") ? Number(data.get("order")) : 100;
const image = data.get("image")
? data.get("image")
: "https://d2cvlmmg8c0xrp.cloudfront.net/web-css/data_engineering_with_aws.jpg";
const amazon = data.get("amazon")
? data.get("amazon")
: "https://www.amazon.com/Data-Engineering-AWS-Gareth-Eagar/dp/1800560419/ref=sr_1_1?crid=28BFB3NXGTM9G&keywords=data+engineering+with+aws&qid=1682772617&sprefix=data+engineering+with+aws%2Caps%2C485&sr=8-1";
const response = await API.graphql({
query: addBook,
variables: {
// id: "112",
title: title,
author: author,
description: description,
order: order,
url: url,
amazon: amazon,
image: image,
},
});
// console.log(response);
redirect("/");
};
export default handleForm;

The form and ui

import handleForm from "./action";
const FormPage = () => {
return (
<div className="dark:bg-slate-800 min-h-screen">
<div className="mx-auto max-w-3xl px-5">
<form action={handleForm}>
<div className="grid gap-6 mb-6 grid-cols-1">
<div>
<label
htmlFor="title"
className="block mb-2 text-sm font-medium dark:text-white"
>
Title
</label>
<input
id="title"
type="text"
placeholder="Machine Learning"
name="title"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="author"
className="block mb-2 text-sm font-medium dark:text-white "
>
Author
</label>
<input
id="author"
type="text"
placeholder="hai tran"
name="author"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="image"
className="block mb-2 text-sm font-medium dark:text-white "
>
Image
</label>
<input
id="image"
type="text"
placeholder="image"
name="image"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="description"
className="block mb-2 text-sm font-medium dark:text-white"
>
Description
</label>
<input
id="description"
type="text"
placeholder="about machine learning"
name="description"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="url"
className="block mb-2 text-sm font-medium dark:text-white"
>
URL
</label>
<input
id="url"
type="text"
placeholder="url"
name="url"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="amazon"
className="block mb-2 text-sm font-medium dark:text-white"
>
Amazon
</label>
<input
id="amazon"
type="text"
placeholder="amazon"
name="amazon"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="Order"
className="block mb-2 text-sm font-medium dark:text-white"
>
Order
</label>
<input
id="order"
type="text"
placeholder="100"
name="order"
className="text-sm rounded-sm block w-full p-2.5 outline-none focus:outline-none focus:border-2 focus:border-blue-500 focus:ring-blue-500 focus:rounded-sm"
></input>
</div>
<div>
<label
htmlFor="upload"
className="block mb-2 text-sm font-medium dark:text-white"
>
Upload File
</label>
<input
id="upload"
type="file"
name="upload"
className="text-sm rounded-sm w-full p-2.5 cursor-pointer dark:bg-white"
></input>
</div>
<div>
<button className="bg-orange-400 px-10 py-3 rounded-sm">
Submit
</button>
</div>
</div>
</form>
</div>
</div>
);
};
export default FormPage;

Reference#