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: Stringauthor: Stringdescription: Stringorder: Inturl: Stringamazon: Stringimage: String}type Message {id: ID!content: String}type PaginatedMessages {messages: [Message]nextToken: String}type Schema {query: Querymutation: Mutation}type Mutation {addMessage(id: ID, content: String): MessageaddBook(id: IDtitle: Stringauthor: Stringdescription: Stringorder: Inturl: Stringamazon: Stringimage: String): Book}type Query {getBook(id: ID!): BooklistBooks: [Book]getMessage(id: ID!): MessagelistMessages(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/cliamplify add codegen --apiId tyweijjzgvfqjeqqat52natp4mamplify 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) => (<divkey={id}className="dark:bg-slate-700 p-5 shadow-lg rounded-sm"><h4 className="font-bold mb-8">{book.title}</h4><div><Imagesrc={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><labelhtmlFor="title"className="block mb-2 text-sm font-medium dark:text-white">Title</label><inputid="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><labelhtmlFor="author"className="block mb-2 text-sm font-medium dark:text-white ">Author</label><inputid="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><labelhtmlFor="image"className="block mb-2 text-sm font-medium dark:text-white ">Image</label><inputid="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><labelhtmlFor="description"className="block mb-2 text-sm font-medium dark:text-white">Description</label><inputid="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><labelhtmlFor="url"className="block mb-2 text-sm font-medium dark:text-white">URL</label><inputid="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><labelhtmlFor="amazon"className="block mb-2 text-sm font-medium dark:text-white">Amazon</label><inputid="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><labelhtmlFor="Order"className="block mb-2 text-sm font-medium dark:text-white">Order</label><inputid="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><labelhtmlFor="upload"className="block mb-2 text-sm font-medium dark:text-white">Upload File</label><inputid="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;