1. 概要
前回は画面の一部をフルスクリーンにする内容についてでした。今回はAmplify(Gen2)やAppSyncを使い、DynamoDBを操作する内容になります。
対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。
2. nodeのインストール
こちらを参考
3. プロジェクトを作成
3-1-1. こちらを参考
4. 必要なライブラリをインストール
npm create amplify@latest
※上記コマンドが正常に実行終わると「amplify_outputs.json」が作成される。
5. Amazon DynamoDBテーブルをセットアップ
5-1-1. こちらを参考
Create Resources

Choose 「Use existing type」

- Primary key : id
- Sort key : None
- Disable Automatically generate GraphQL

Create

6. ソースコード
※Amplifyで進めると、デフォルトで「auth」と「data」が存在するが、今回は「auth」を削除し、「data」のみを扱ってみる。
6-1. データソースを追加
6-1-1. amplify/backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { data } from "./data/resource";
import { aws_dynamodb } from "aws-cdk-lib";
export const backend = defineBackend({
data,
});
const externalDataSourcesStack = backend.createStack("MyExternalDataSources");
const externalTable = aws_dynamodb.Table.fromTableName(
externalDataSourcesStack,
"MyExternalPostTable",
"PostTable"
);
backend.data.addDynamoDbDataSource(
"ExternalPostTableDataSource",
externalTable
);
6-2. データリソースを追加
- customType
- queries
- mutations
- subscriptions
6-2-1. amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Todo: a
.model({
content: a.string(),
isDone: a.boolean(),
})
.authorization((allow) => [allow.publicApiKey()]),
Post: a.customType({
id: a.id().required(),
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
ups: a.integer(),
downs: a.integer(),
version: a.integer(),
}),
ReturnData: a.customType({
mutationType: a.string().required(),
post: a.ref("Post"),
}),
PaginatedPosts: a.customType({
posts: a.ref("Post").array(),
nextToken: a.string(),
}),
createPost: a
.mutation()
.arguments({
id: a.id(),
author: a.string().required(),
title: a.string(),
content: a.string(),
url: a.string(),
})
.returns(a.ref("ReturnData"))
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./createPost.js",
})
),
getPost: a
.query()
.arguments({ id: a.id().required() })
.returns(a.ref("Post"))
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./getPost.js",
})
),
listPosts: a
.query()
.arguments({
limit: a.integer(),
nextToken: a.string(),
})
.returns(a.ref("PaginatedPosts"))
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./listPosts.js",
})
),
updatePost: a
.mutation()
.arguments({
id: a.id().required(),
author: a.string(),
title: a.string(),
content: a.string(),
url: a.string(),
expectedVersion: a.integer().required(),
})
.returns(a.ref("ReturnData"))
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./updatePost.js",
})
),
deletePost: a
.mutation()
.arguments({ id: a.id().required(), expectedVersion: a.integer() })
.returns(a.ref("ReturnData"))
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
dataSource: "ExternalPostTableDataSource",
entry: "./deletePost.js",
})
),
onSubscribePost: a
.subscription()
.for([a.ref("createPost"), a.ref("updatePost"), a.ref("deletePost")])
.arguments({})
.authorization((allow) => [allow.publicApiKey()])
.handler(
a.handler.custom({
entry: "./subscribePost.js",
})
),
});
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: "apiKey",
apiKeyAuthorizationMode: {
expiresInDays: 30,
},
},
name: "MyPostApi",
});
6-3. カスタムハンドラーを作成
6-3-1. amplify/data/listPost.js
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
const { limit = 20, nextToken } = ctx.arguments;
return ddb.scan({ limit, nextToken });
}
export function response(ctx) {
const { items = [], nextToken } = ctx.result;
return { posts: items, nextToken };
}
6-3-2. amplify/data/getPost.js
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
return ddb.get({ key: { id: ctx.args.id } });
}
export const response = (ctx) => ctx.result;
6-3-3. amplify/data/createPost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
const item = { ...ctx.arguments, ups: 1, downs: 0, version: 1 };
const key = { id: ctx.args.id ?? util.autoId() };
const post = ddb.put({ key, item });
return post;
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
util.appendError(error.message, error.type);
}
return { mutationType: "CREATE", post: result };
}
6-3-4. amplify/data/deletePost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
let condition = null;
if (ctx.args.expectedVersion) {
condition = {
or: [
{ id: { attributeExists: false } },
{ version: { eq: ctx.args.expectedVersion } },
],
};
}
return ddb.remove({ key: { id: ctx.args.id }, condition });
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
util.appendError(error.message, error.type);
}
return { mutationType: "DELETE", post: result };
}
6-3-5. amplify/data/updatePost.js
import { util } from "@aws-appsync/utils";
import * as ddb from "@aws-appsync/utils/dynamodb";
export function request(ctx) {
const { id, expectedVersion, ...rest } = ctx.args;
const values = Object.entries(rest).reduce((obj, [key, value]) => {
obj[key] = value ?? ddb.operations.remove();
return obj;
}, {});
return ddb.update({
key: { id },
condition: { version: { eq: expectedVersion } },
update: { ...values, version: ddb.operations.increment(1) },
});
}
export function response(ctx) {
const { error, result } = ctx;
if (error) {
util.appendError(error.message, error.type);
}
return { mutationType: "UPDATE", post: result };
}
6-3-6. amplify/data/subscribePost.js
export function request(ctx) {
return {};
}
export function response(ctx) {
return null;
}
6-4. クライアント側のソースコードを作成
6-4-1. src/app/amplify-configure.ts
import { Amplify } from "aws-amplify";
import outputs from "../../amplify_outputs.json";
const configureAmplify = () => {
Amplify.configure(outputs);
};
export { configureAmplify };
6-4-2. src/app/app.css
body {
margin: 0;
background: linear-gradient(180deg, rgb(117, 81, 194), rgb(255, 255, 255));
display: flex;
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
height: 100vh;
width: 100vw;
justify-content: center;
align-items: center;
}
main {
display: flex;
flex-direction: column;
align-items: stretch;
}
input {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
margin-right: 8px;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
color: white;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
ul {
padding-inline-start: 0;
margin-block-start: 0;
margin-block-end: 0;
list-style-type: none;
display: flex;
flex-direction: column;
margin: 8px 0;
border: 1px solid black;
gap: 1px;
background-color: black;
border-radius: 8px;
overflow: auto;
}
li {
background-color: white;
padding: 8px;
}
li:hover {
background: #dadbf9;
}
a {
font-weight: 800;
text-decoration: none;
}
6-4-3. src/app/page.tsx
"use client";
import {
ChangeEvent,
MouseEvent,
KeyboardEvent,
useState,
useEffect,
} from "react";
import { configureAmplify } from "@/app/amplify-configure";
import { generateClient } from "aws-amplify/api";
import { Schema } from "../../amplify/data/resource";
import "@/app/app.css";
configureAmplify();
const Home = () => {
const client = generateClient<Schema>();
const [content, setContent] = useState<string>("");
const [posts, setPosts] = useState<Array<Schema["Post"]["type"]>>();
const listPosts = async (limit: number) => {
const { data, errors } = await client.queries.listPosts({ limit });
if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
if (data) {
const postData = data.posts as Array<Schema["Post"]["type"]>;
postData.map((post) => {
setPosts((prev) => [...(prev ?? []), post]);
});
}
};
useEffect(() => {
listPosts(10);
}, []);
const getPost = async (id: string, e: MouseEvent<HTMLLIElement>) => {
e.preventDefault();
const { data, errors } = await client.queries.getPost({
id,
});
if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
console.log(`[DATA]:${JSON.stringify(data)}`);
};
const addPost = async () => {
if (content.length == 0) return;
const { data, errors } = await client.mutations.createPost({
title: "My Post",
content,
author: "Sondon",
});
if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
console.log(`[DATA]:${JSON.stringify(data)}`);
setContent("");
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
setContent(e.currentTarget.value);
};
const keyDownHandlerPost = async (e: KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing || e.key !== "Enter") return;
await addPost();
};
const createPost = async (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
await addPost();
};
const deletePost = async (id: string, e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const { data, errors } = await client.mutations.deletePost({
id,
});
if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
console.log(`[DATA]:${JSON.stringify(data)}`);
};
useEffect(() => {
const sub = client.subscriptions.onSubscribePost({}).subscribe({
next: ({ mutationType, post }) => {
if (post) {
setPosts((prev) => {
switch (mutationType) {
case "CREATE":
return [...(prev ?? []), post];
case "DELETE":
return prev?.filter((p) => p.id !== post.id);
}
});
}
},
error: (error) => {
console.error(`[ERROR] ${JSON.stringify(error)}`);
},
});
return () => sub.unsubscribe();
}, []);
return (
<div>
<h2>My posts</h2>
<input
type="text"
value={content}
onChange={(e) => handleChange(e)}
onKeyDown={keyDownHandlerPost}
autoFocus={true}
/>
<button onClick={(e) => createPost(e)}>Register</button>
<ul>
{posts &&
posts.map((post) => (
<li key={post.id} onClick={(e) => getPost(post.id, e)}>
<div
style={{
display: "flex",
flexWrap: "wrap",
overflow: "hidden",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>{post.content}</div>
<div>
<button onClick={(e) => deletePost(post.id, e)}>
Delete
</button>
</div>
</div>
</li>
))}
</ul>
</div>
);
};
export default Home;
7. サーバーを起動
7-1-1. Sandboxを立ち上げる
npx ampx sandbox --identifier appsyncDydbDemo
7-2-1. NextJSを立ち上げる
npm run dev
8. ブラウザで確認
- http://localhost:3000
8-1-1. 画面

8-2-1. DynamoDB

8-3-1. AppSync

9. ディレクトリの構造
.
├── README.md
├── amplify
│ ├── auth
│ │ └── resource.ts
│ ├── backend.ts
│ ├── data
│ │ ├── createPost.js
│ │ ├── deletePost.js
│ │ ├── getPost.js
│ │ ├── listPosts.js
│ │ ├── resource.ts
│ │ ├── subscribePost.js
│ │ └── updatePost.js
│ ├── package.json
│ └── tsconfig.json
├── amplify_outputs.json
├── eslint.config.mjs
├── next-env.d.ts
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── src
│ └── app
│ ├── amplify-configure.ts
│ ├── app.css
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
└── tsconfig.json
6 directories, 31 files
10. 備考
今回はAmplify(Gen2)やAppSyncを使い、DynamoDBを操作する内容についてでした。
11. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Getting Started with Redux | Redux
- Getting Started with React Redux | React Redux (react-redux.js.org)
- Material UI: React components based on Material Design (mui.com)
- Connect to external Amazon DynamoDB data sources – AWS Amplify Gen 2 Documentation
- AWS AppSync Documentation
投稿者プロフィール

-
開発好きなシステムエンジニアです。
卓球にハマってます。