【NextJS】Access AppSync API with DynamoDB using Amplify(Gen1)

1. 概要

前回はAmplify(Gen2)やAppSyncを使い、DynamoDBを操作する内容でした。今回は前回作成したAppSync APIにアクセスする内容となります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. nodeのインストール

こちらを参考

3. プロジェクトを作成

3-1-1. こちらを参考

4. 必要なライブラリをインストール

npm install aws-amplify

5. アプリと統合する

※前回作成したAPI画面の内容に従い、セットアップする

5-1. クライアントを接続

5-1-1. API設定のスニペットを使用して Amplify をセットアップ

import { Amplify } from 'aws-amplify';

Amplify.configure({
  API: {
    GraphQL: {
      endpoint: 'https://12345678901234567890123456.appsync-api.ap-northeast-1.amazonaws.com/graphql',
      region: 'ap-northeast-1',
      defaultAuthMode: 'apiKey',
      apiKey: 'ab2-abcdefghijklmnopqrstuvwxyz'
    }
  }
});

5-1-2. codegenを生成

npx @aws-amplify/cli codegen add --apiId 12345678901234567890123456 --region ap-northeast-1

※デフォルトでクライアントヘルパーコードが「src/graphql」フォルダに生成される。

※APIをデプロイするたびに、次のコマンドを再実行して、更新されたGraphQLステートメントとタイプを生成

npx @aws-amplify/cli codegen

6. ソースコード

6-1. 上記コマンド実行より自動生成

  • src
    • API.ts
    • graphql
      • mutations.ts
      • queries.ts
      • subscriptions.ts

6-2. クライアント側のソースコードを作成

6-2-1. src/app/appsync-client.ts

import { Amplify } from "aws-amplify";
import { generateClient } from "aws-amplify/api";

Amplify.configure({
  API: {
    GraphQL: {
      endpoint: 'https://12345678901234567890123456.appsync-api.ap-northeast-1.amazonaws.com/graphql',
      region: 'ap-northeast-1',
      defaultAuthMode: 'apiKey',
      apiKey: 'ab2-abcdefghijklmnopqrstuvwxyz'
    },
  },
});

const appsyncClient = generateClient();

export { appsyncClient };

6-2-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-2-3. src/app/page.tsx

"use client";

import {
  ChangeEvent,
  MouseEvent,
  KeyboardEvent,
  useState,
  useEffect,
} from "react";
import { appsyncClient } from "./appsync-client";
import { listPosts, getPost } from "../graphql/queries";
import { createPost, updatePost, deletePost } from "@/graphql/mutations";
import { onSubscribePost } from "@/graphql/subscriptions";
import { Post } from "@/API";
import "@/app/app.css";

export default function Home() {
  const [content, setContent] = useState<string>("");
  const [posts, setPosts] = useState<Post[]>();

  const getListPosts = async (limit: number) => {
    const { data, errors } = await appsyncClient.graphql({
      query: listPosts,
      variables: {
        limit,
      },
    });
    if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
    if (data) {
      const postData = data.listPosts?.posts as Array<Post>;
      postData.map((post) => {
        setPosts((prev) => [...(prev ?? []), post]);
      });
    }
  };

  useEffect(() => {
    getListPosts(10);
  }, []);

  const getPostData = async (id: string, e: MouseEvent<HTMLLIElement>) => {
    e.preventDefault();
    const { data, errors } = await appsyncClient.graphql({
      query: getPost,
      variables: {
        id,
      },
    });
    if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
  };

  const applyPost = async () => {
    if (content.length == 0) return;
    const { data, errors } = await appsyncClient.graphql({
      query: createPost,
      variables: {
        title: "My Post",
        content,
        author: "Sondon",
      },
    });
    if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
    setContent("");
  };

  const keyDownHandlerPost = async (e: KeyboardEvent<HTMLInputElement>) => {
    if (e.nativeEvent.isComposing || e.key !== "Enter") return;
    await applyPost();
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    e.preventDefault();
    setContent(e.currentTarget.value);
  };

  const createPostData = async (e: MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    await applyPost();
  };

  const updatePostData = async (
    id: string,
    author: string,
    expectedVersion: number,
    content: string,
    e: MouseEvent<HTMLButtonElement>
  ) => {
    e.preventDefault();
    const { data, errors } = await appsyncClient.graphql({
      query: updatePost,
      variables: {
        id,
        author,
        content,
        expectedVersion,
      },
    });
    if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
  };

  const deletePostData = async (
    id: string,
    e: MouseEvent<HTMLButtonElement>
  ) => {
    e.preventDefault();
    const { data, errors } = await appsyncClient.graphql({
      query: deletePost,
      variables: {
        id,
      },
    });
    if (errors) console.error(`[ERROR]:${JSON.stringify(errors)}`);
  };

  useEffect(() => {
    const sub = appsyncClient.graphql({ query: onSubscribePost }).subscribe({
      next: ({ data }) => {
        if (!data.onSubscribePost) return;
        const { mutationType, post } = data.onSubscribePost;
        if (post) {
          setPosts((prev) => {
            switch (mutationType) {
              case "CREATE":
                return [...(prev ?? []), post];
              case "UPDATE":
                return prev?.map((p) => (p.id === post.id ? post : p));
              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) => createPostData(e)}>Register</button>
      <ul>
        {posts &&
          posts.map((post) => (
            <li key={post.id} onClick={(e) => getPostData(post.id, e)}>
              <div
                style={{
                  display: "flex",
                  flexWrap: "wrap",
                  overflow: "hidden",
                  justifyContent: "space-between",
                  alignItems: "center",
                }}
              >
                <div>{post.content}</div>
                <div>
                  <button onClick={(e) => deletePostData(post.id, e)}>
                    Delete
                  </button>
                </div>
              </div>
            </li>
          ))}
      </ul>
    </div>
  );
}

7. サーバーを起動

7-1-1. NextJSを立ち上げる

npm run dev

8. ブラウザで確認

  • http://localhost:3000

8-1-1. 画面

9. ディレクトリの構造

.
├── README.md
├── 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
├── schema.json
├── src
│   ├── API.ts
│   ├── app
│   │   ├── app.css
│   │   ├── appsync-client.ts
│   │   ├── favicon.ico
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── graphql
│       ├── mutations.ts
│       ├── queries.ts
│       └── subscriptions.ts
└── tsconfig.json

4 directories, 24 files

10. 備考

今回は前回作成したAppSync APIにアクセスする内容についてでした。

11. 参考

投稿者プロフィール

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

関連記事

  1. 【NextJS】Hooks-useState(update separ…

  2. 【NextJS】Canvasを使い、図形を描画

  3. 【NextJS】Online HTML Editor with Tip…

  4. 【NextJS】Error Handling

  5. 【NextJS】Fullscreen of a portion of …

  6. 【NextJS】Button・IconButton・LoadingBu…

最近の記事

  1. Node.js
  2. AWS

制作実績一覧

  1. Checkeys