見出し画像

今から始めるServerless Frameworkでサーバーレスデビュー【フロントエンド編#2】

まだバックエンド編を見ていない方は先にバックエンド編を見てください。

● 今から始めるServerless Frameworkでサーバーレスデビュー【バックエンド編#1】
https://note.com/ryoppei/n/n9163712b68ad

前編ではServerless Frameworkを使用してAWS上に環境を構築しました。
今回はその構築された環境にフロントエンドから接続してみたいと思います。
Cognitoを使用した認証やGraphQL APIを介してDynamoDBへのCRUD操作を行ってみます。
まずはReact Create Appを使用してReactの開発環境をセットアップします。
前回のバックエンド編で構築したプロジェクトにReact環境を追加することもできますが、実際のプロダクトでは複数のフロントエンド(WebやiOS/Androidアプリなど)が存在することがよくあります。

そのため、バックエンドのプロジェクトとは別にcreate-react-appを使用して新しいプロジェクトを作成します。

1.とりあえずCreate React Appする

$ npx create-react-app my-serverless-web --typescript

これでReactとTypeScriptの開発環境が整いました。

2.バックエンド側から設定ファイルをコピーする

バックエンド編でAmplifyのプラグインを導入することで生成されたaws-exports.jsファイルを、フロントエンドの/srcディレクトリにコピーします。
これにより、ReactとAmplifyを連携するための準備が整いました。

3.Amplify CLIを導入する

次にフロント側からバックエンド側にアクセスするため、Amplify CLIをインストールします。

少しややこしい点がありますので、注意点を紹介します。
Amplify CLIは非常に便利で、Amplify CLIを使用してバックエンドの環境を構築することもできます。
しかし、今回はServerless Frameworkを使用してバックエンドの環境を構築しているため、Amplify CLIは認証やGraphQLの操作に限定して使用することになります。

$ npm i -g @aws-amplify/cli

メッセージの対話コマンドを進めていきますので、選択肢に従って進めてください。

$ amplify configure // AmplifyCLIで利用するユーザーを作成します。

$ amplify init // Amplifyを初期化します。

? Enter a name for the environment → dev
? Choose your default editor: → Visual Studio Code
? Choose the type of app that you're building → javascript
Please tell us about your project
? What javascript framework are you using → React
? Source Directory Path: → src
? Distribution Directory Path: → build
? Build Command: npm run-script → build
? Start Command: npm run-script → serve

Amplifyの基本的な設定が完了したので、次はGraphQLを動作させる準備をします。

4.AWSのAppSyncのコンソールに進む

表示されているコマンドをコピーして実行します。
なお、一部は伏せ字になっています。

$ amplify add codegen --apiId **************

再び対話コマンドになりますので、選択肢に従って進めます。
処理が完了すると、以下のファイルとディレクトリが生成されます。

src
├── API.ts
├── graphql
│     ├── mutations.ts
│     ├── queries.ts
│     └── schema.json
└── schema.graphql

これでGraphQLを使用する準備が整いました。

5.ReactとAmplifyを繋ぐ​

$ npm i aws-amplify // aws-amplifyをインストール
// index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './pages/App/App';
import * as serviceWorker from './serviceWorker';

import Amplify from 'aws-amplify'; // aws-amplifyをインポート
import { Auth } from '@aws-amplify/auth' // aws-amplify/authをインポート
import config from './aws-exports'; // バックエンドからコピーしてきた設定ファイル

Amplify.configure(config) // 設定ファイルをもとにAmplifyに設定
Auth.configure(config); // 設定ファイルをもとにAmplifyに設定

ReactDOM.render(
<React.StrictMode>
  <App />
</React.StrictMode>,
document.getElementById('root')
);

serviceWorker.unregister();

こうしてバックエンド編で構築した環境にフロントエンドから接続することに成功しました。
しかし、まだGraphQL APIを叩くことはできません。
なぜなら、バックエンドの環境構築時にCognitoを使用して認証を導入しているため、Cognitoのユーザー認証を通過しないとGraphQLを使用することができないように設定されているからです。

6.認証画面を作っていく

①認証ロジックを作る。
②SignIn画面とSignUp画面を作る。
③SignUp完了後メールに送られるリンクから遷移してくるSignUpConfirm画面を作る。

それでは、順を追って進めていきましょう。
まずは認証に関するロジックをCustom Hooksとして作成していきます。

// src/hooks/useAuth.ts

import { useCallback } from 'react';
import { Auth } from 'aws-amplify';
import { CognitoUserSession } from "amazon-cognito-identity-js";
import { useDispatch } from 'react-redux';
import { useHistory } from "react-router";
import { AuthActions } from '../stores/auth/actions';
import { useSelector } from './useSelector';
import * as PAGE from '../constants/index';
import { initialState } from '../stores/auth/reducers';

//------------------------------
// Type
//------------------------------
export type useAuthType = {
    initUser: () => Promise<void>
    signIn: (username: string, password: string) => Promise<void>
    signUp: (username: string, password: string, email: string) => Promise<void>
    signOut: () => void
    resendSignUp: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => Promise<void>
}

//------------------------------
// Hooks
//------------------------------
export const useAuth = (): useAuthType => {
const dispatch = useDispatch()
const selector = useSelector()
const history = useHistory()

/**
 * ユーザー初期化処理
 */
const initUser = useCallback(async (): Promise<void> => {
  const auth: CognitoUserSession | null = await Auth.currentSession()
  const { username } = auth.getAccessToken().payload
  const { email } = auth.getIdToken().payload

  if (auth) {
    dispatch(AuthActions.authenticated({
      auth: auth,
      username: username,
      email: email,
      isAuth: true
    }))
  }
}, [dispatch])

/**
 * サインイン処理
 */
const signIn = async (_username: string, _password: string): Promise<void> => {
  try {
    await Auth.signIn(_username, _password).then(() => {
      initUser()
      history.push(PAGE.PAGE_HOME)
    })
  } catch (e) {
    alert('ログインに失敗しました')
    window.location.reload()
    return
  }
}

/**
 * サインアップ処理
 */
const signUp = async (_username: string, _password: string, _email: string): Promise<void> => {
  try {
    const newUser = await Auth.signUp({
      username: _username,
      password: _password,
      attributes: {
        email: _email
      }
    })

    if (newUser) {
      history.push(PAGE.PAGE_SIGNUP_COMPLETE)
    }
  } catch (e) {
    alert('ユーザーの作成に失敗しました')
    console.log(e)
    return
  }
}

/**
 * 再度認証メールを送る処理
 */
const resendSignUp = async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
  e.preventDefault()

  const username = selector.userState().username
  if (username) {
    await Auth.resendSignUp(username)
    alert('メールアドレスに確認コードを再送信しました')
    history.push(PAGE.PAGE_HOME)
  }
}

/**
 * サインアウト処理
 */
const signOut = (): void => {
  Auth.signOut()
  history.push(PAGE.PAGE_SIGNIN)
  dispatch(AuthActions.authenticated(initialState))
}

return { initUser, signIn, signUp, signOut, resendSignUp }
}

次に、SignIn、SignUp、SignUpConfirmの各画面とそれに対応するHooksを作成していきましょう。

// src/pages/SignIn.tsx

import React, { useState } from 'react';
import { TextField, Button, Container, Box, Typography } from '@material-ui/core';
import { Wrapper } from '../../components/Wrapper';
import { Link } from 'react-router-dom';
import { useAuth } from '../../hooks/useAuth';

//------------------------------
// Component
//------------------------------
const SignIn = () => {

//------------------------------
// Hooks
//------------------------------
const auth = useAuth()
const [name, setName] = useState<string>('')
const [password, setPassword] = useState<string>('')

  return (
    <Wrapper>
      <Container maxWidth="sm">
        <Typography component={"h2"} variant={"h5"} align={"center"}>
          <Box fontWeight="fontWeightBold">
            SignIn - ログインする
          </Box>
        </Typography>
        <Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>USER NAME</Typography>
            <Box mt={1}>
              <TextField
                variant={'outlined'}
                value={name}
                onChange={(e) => setName(e.target.value)}
                fullWidth
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>PASSWORD</Typography>
            <Box mt={1}>
              <TextField
                type="password"
                value={password}
                variant={'outlined'}
                onChange={(e) => setPassword(e.target.value)}
                fullWidth
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Button
              variant="contained"
              color="primary" onClick={() => auth.signIn(name, password)}
              fullWidth
            >
              ログイン
            </Button>
          </Box>

          <Box mt={2} textAlign={"center"}>
            <Link to={'/signup'}>アカウントをお持ちで無い場合はコチラ</Link>
          </Box>
        </Box>
      </Container>
    </Wrapper>
  );
}

export default SignIn;
// src/pages/SignUp.tsx

import React, { useState } from 'react';
import { Box, Container, Button, TextField, Typography } from '@material-ui/core';
import { useAuth } from '../../hooks/useAuth';
import { Wrapper } from '../../components/Wrapper';

//------------------------------
// Component
//------------------------------
const SignUp = () => {

//------------------------------
// Hooks
//------------------------------
const auth = useAuth()
const [username, setUsername] = useState<string>('')
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')

  return (
    <Wrapper>
      <Container maxWidth="sm">
        <Typography component={"h2"} variant={"h5"} align={"center"}>
          <Box fontWeight="fontWeightBold">
            SignUp - 新規登録する
          </Box>
        </Typography>
        <Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>USER NAME</Typography>
            <Box mt={1}>
              <TextField
                type="text"
                variant={'outlined'}
                value={username}
                onChange={(e) => setUsername(e.target.value)}
                fullWidth
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>PASSWORD</Typography>
            <Box mt={1}>
              <TextField
                type="password"
                variant={'outlined'}
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                fullWidth
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>E-MAIL</Typography>
            <Box mt={1}>
              <TextField
                type="email"
                variant={'outlined'}
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                fullWidth
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Button
              variant="contained"
              color="primary"
              onClick={() => auth.signUp(username, password, email)}
              fullWidth
            >
              登録する
            </Button>
          </Box>
        </Box>
      </Container>
    </Wrapper>
  );
}

export default SignUp
// src/pages/SignUpConfirm.tsx 

import React from 'react';
import { TextField, Button, Container, Box, Typography } from '@material-ui/core';
import { Wrapper } from '../../components/Wrapper';
import { Link } from 'react-router-dom';
import { useConfirm } from '../../hooks/useConfirm';

//------------------------------
// Component
//------------------------------
const SignUpConfirm = () => {

//------------------------------
// Hooks
//------------------------------
const confirm = useConfirm()

  return (
    <Wrapper>
      <Container maxWidth="sm">
        <Typography component={"h2"} variant={"h5"} align={"center"}>
          <Box fontWeight="fontWeightBold">
            Confirm - メールに届いた認証コードを入力する
          </Box>
        </Typography>
        <Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>ユーザーID(メールアドレス)</Typography>
            <Box mt={1}>
              <TextField
                variant={'outlined'}
                value={confirm.getConfirmParams().username}
                fullWidth
                disabled
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Typography component={"p"} variant={"body1"}>認証コード</Typography>
            <Box mt={1}>
              <TextField
                type="password"
                value={confirm.getConfirmParams().code}
                variant={'outlined'}
                fullWidth
                disabled
              />
            </Box>
          </Box>
          <Box mt={4}>
            <Button
              variant="contained"
              color="primary"
              onClick={() => confirm.confirm(confirm.username, confirm.code)}
              fullWidth
            >
              ユーザー登録を完了する
            </Button>
          </Box>

          <Box mt={2} textAlign={"center"}>
            <Link to={''} onClick={(e) => confirm.resendConfirm(e)}>認証コードを再発行する</Link>
          </Box>
        </Box>
      </Container>
    </Wrapper>
  );
}

export default SignUpConfirm;
// src/hooks/useConfirm.ts

import { useEffect, useState } from 'react';
import { Auth } from 'aws-amplify';
import { useHistory, useLocation } from "react-router";
import * as PAGE from '../constants/index';

//------------------------------
// Type
//------------------------------
export type useConfirmType = {
  isConfirmParams: () => boolean
  getConfirmParams: () => confirmParams
  confirm: (email: string, code: string) => Promise<void>
  resendConfirm: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => Promise<void>
  username: string
  code: string
}

export type confirmParams = {
  username: string | null
  code: string | null
}

//------------------------------
// Hooks
//------------------------------
export const useConfirm = (): useConfirmType => {
  const history = useHistory()
  const location = useLocation()
  const [username, setUsername] = useState<string>('')
  const [code, setCode] = useState<string>('')

  //------------------------------
  // LifeCycle
  //------------------------------
  useEffect(() => {
    // 確認メールのURLから飛んできたかを確認する
    const params = new URLSearchParams(location.search)
    const username = params.get('username')
    const code = params.get('code')
    if (username) {
      setUsername(username)
    } else {
      history.push(PAGE.PAGE_HOME)
      window.scrollTo(0, 0)
    }
    if (code) {
      setCode(code)
    }
  }, [history, location.search])

  /**
   * 確認メールのURLから取得したParamsを返す
   */
  const getConfirmParams = (): confirmParams => {
    return {
      username: username,
      code: code
    }
  }

  /**
   * URLのクエリパラメータが存在するかどうか
   */
  const isConfirmParams = (): boolean => {
    return username ? true : false
  }

  /**
   * コンファーム処理
   */
  const confirm = async (inputEmail: string, inputCode: string): Promise<void> => {
    try {
      const confirmUser = await Auth.confirmSignUp(inputEmail, inputCode)
      if (confirmUser) {
        history.push(PAGE.PAGE_HOME)
      }
    } catch (e) {
      console.log(e)
    }
  }

  /**
   * 再度認証メールを送る処理
   */
  const resendConfirm = async (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
    e.preventDefault()
    if (username) {
      await Auth.resendSignUp(username)
      alert('メールアドレスに確認コードを再送信しました')
      history.push(PAGE.PAGE_SIGNIN)
    }
  }

  return {
    isConfirmParams,
    getConfirmParams,
    confirm,
    resendConfirm,
    username,
    code
  }
}

これで画面と認証のロジックが完成しました。
実際の遷移や画面制御を行いながら確認するために、Routerを導入しましょう。

$ npm install react-router-dom

①ReactにRouterを導入してログインしていなければSignIn画面へ。
②ログインしていない場合はSignIn画面へと遷移、SignIn画面ではSignUp画面へと遷移するリンクを用意。
③SignUp画面で必要な情報を入力して問題無ければTOPページへ。

といったフローを行います。
③の時に、上手くユーザー登録が行われた場合には、バックエンド側で設定していたfunctionsが起動します。

functions:

# ユーザー作成認証する処理
customMessages:
  handler: functions/cognito.handler
  memorySize: 128
  timeout: 10
  events:
    - cognitoUserPool:
        pool: ${self:service}-user-pool-${opt:stage, self:provider.stage}
        trigger: CustomMessage
        existing: true

このLambda関数は、ユーザーのSingUpが行われたときに送信されるトリガーを感知して発火します。
このトリガーは、「ユーザー登録はされたけど、ユーザーの確認が行われていない」というタイミングで送信されるメールです。
しかし、このユーザー確認のためのCognito自動送信メールは英語でのメールとなってしまっています。
メール内容を変更したり、日本語に変更するためには、Lambdaからカスタムなメッセージを送信する必要があります。

今回は、以下のようなメールの内容を自動送信することにします。

● 新規登録の確認コード
下記URLからユーザー登録を完了してください。
URL: http://localhost:3000/signup/confirm?email=example.email%40gmail.com&code=157947

メールアドレス宛に送信されたURLをクリックすると、ユーザーの確認が完了する画面に遷移します。
そして、ユーザーの確認が完了したら、SignIn画面にリダイレクトされます。
これでユーザーは登録から確認までが完了し、ログインすることができるようになりました。

7.GraphQL APIを叩く

それでは、最後に簡単な例としてGraphQL APIを実際に叩いてみましょう。

①GraphQLでCRUDするロジックを作る。
②画面上で実際にCRUDできるようにする。

AmplifyとAppSyncを結びつける際に、Schemaから生成されたqueries.tsやmutations.tsといったファイルにはGraphQLを実行するためのメソッドが用意されていますので、それらを活用していきましょう。
また、API.tsファイルにはSchemaから自動生成されたqueriesやmutationsの型情報が含まれていますので、必要に応じてインポートして使用します。

// src/hooks/usePost.ts

import { useState } from 'react';
import { API, graphqlOperation } from "aws-amplify";
import * as queries from '../graphql/queries';
import * as mutations from '../graphql/mutations';
import * as graphAPI from '../API'

//------------------------------
// Type
//------------------------------
export type usePostsType = {
    //------------------------------
    // Handler
    //------------------------------
    onCreate: () => Promise<void>
    onUpdate: (postId: string) => Promise<void>
    onDelete: (postId: string | undefined) => Promise<void>
    
    //------------------------------
    // Getter
    //------------------------------
    getListPost: () => graphAPI.ListPostQuery | undefined
    getPost: () => string
    getUpdatePost: () => {[key: string]: string}
    
    //------------------------------
    // Setter
    //------------------------------
    setLists: (lists: graphAPI.ListPostQuery) => void
    setPost: (str: string) => void
    setUpdatePost: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
    
    //------------------------------
    // Initilize
    //------------------------------
    initFetch: (mount: React.MutableRefObject<boolean>) => Promise<void>
}

//------------------------------
// Hooks
//------------------------------
export const usePosts = (): usePostsType => {

    const [_lists, _setLists] = useState<graphAPI.ListPostQuery | undefined>()
    const [_post, _setPost] = useState<string>('')
    const [_updatePost, _setUpdatePost] = useState<{[key: string]: string}>({})
    
    //------------------------------
    // Handler
    //------------------------------
    /**
     * 投稿する処理
     */
    const onCreate = async(): Promise<void> => {
      if (_post.length === 0) {
        alert('1文字以上入力してください')
        return
      }
      await API.graphql(graphqlOperation(mutations.createPost, { input: { content: _post }}))
      _setPost('')
    }
    
    /**
     * 投稿を更新する処理
     */
    const onUpdate = async(postId: string): Promise<void> => {
      if (_updatePost[`post-${postId}`]?.length === 0) {
        alert('1文字以上入力してください')
        return
      }
      await API.graphql(graphqlOperation(mutations.updatePost, { input: { content: _updatePost[`post-${postId}`], postId: postId }}))
      _setUpdatePost({})
      alert('投稿を更新しました')
      window.location.reload()
    }
    
    /**
     * 投稿を削除する処理
     */
    const onDelete = async(postId: string | undefined): Promise<void> => {
      await API.graphql(graphqlOperation(mutations.deletePost, { input: { postId: postId } }))
      window.location.reload()
    }
    
    //------------------------------
    // Getter
    //------------------------------
    /**
     * postのstateを返す
     */
    const getListPost = (): graphAPI.ListPostQuery | undefined => {
      if(_lists !== undefined) {
        return _lists
      }
      return
    }
    
    /**
     * 新規投稿のpostのstateを返す
     */
    const getPost = (): string => {
      return _post
    }
    
    /**
     * リストのpostのstateを返す
     */
    const getUpdatePost = (): {[key: string]: string} => {
      return _updatePost
    }
    
    //------------------------------
    // Setter
    //------------------------------
    /**
     * 投稿をセットする処理
     */
    const setLists = (lists: graphAPI.ListPostQuery | undefined): void => {
      if (!undefined) {
        _setLists(lists)
      }
    }
    
    /**
     * 入力した文字列をセットする処理
     */
    const setPost = (str: string): void => {
      _setPost(str)
    }
    
    /**
     * 各リスト投稿を編集時に入力した文字列をセットする処理
     */
    const setUpdatePost = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
      const value = e.target.value
      // フォーム側のname属性をキーに入力された値をvalueにする
      _setUpdatePost({
        [e.target.name]: value
      })
    }
    
    //------------------------------
    // Initilize
    //------------------------------
    /**
     * 投稿を初期化時に読み込む処理
     */
    const initFetch = async (mount: React.MutableRefObject<boolean>): Promise<void> => {
      const posts = await API.graphql(graphqlOperation(queries.listPost)) as {
        data: graphAPI.ListPostQuery
      }
      if (mount.current) {
        setLists(posts.data)
    
        // フォーム側のname属性とキーを一致させておいてuser.nameを初期値に入れる
        posts.data.listPost.forEach((post) => {
          _setUpdatePost({
            [`post-${post.postId}`]: post.content
          })
        })
      }
    }
    
    return {
      setLists,
      setPost,
      setUpdatePost,
      getPost,
      getUpdatePost,
      getListPost,
      onCreate,
      onUpdate,
      onDelete,
      initFetch
    }
}

それでは、次に画面を作成していきましょう。

// src/pages/Home.tsx

import React, { useEffect, useRef } from 'react';
import { Box, TextField, Button, Container, Typography, Divider } from '@material-ui/core';
import { Modal } from '../../components/Modal';
import { useAuth } from '../../hooks/useAuth';
import { usePosts } from '../../hooks/usePosts';
import { useModal } from '../../hooks/useModal';
import { useModalEpic } from '../../hooks/epic/useModalEpic';
import { useSelector } from '../../hooks/useSelector';


//------------------------------
// Component
//------------------------------
const Home = () => {

    //------------------------------
    // Hooks
    //------------------------------
    const mount = useRef<boolean>(true)
    const posts = usePosts()
    const auth = useAuth()
    const modal = useModal()
    const selector = useSelector()
    const userSelector = selector.userState()
    const epic = useModalEpic(posts.onCreate, modal.onToggleModal)
    
    //------------------------------
    // LifeCycle
    //------------------------------
    useEffect(() => {
      posts.initFetch(mount)
      return () => {
        mount.current = false
      }
      // eslint-disable-next-line
    },[])
    
    return (
      <Box my={8}>
        <Modal callback={modal.onToggleModal} />
        <Container>
          <Typography component={"h2"} variant={"h5"}>
            <Box fontWeight="fontWeightBold">
              {userSelector.username}でログイン中です
            </Box>
            <Box py={4}><Divider /></Box>
          </Typography>
          <ul>{
            posts.getListPost()?.listPost?.map((post, index) => {
              return (
                <Box key={index} my={4}>
                  <li>
                    <Box mb={2}>
                      <Box mb={1} fontWeight="fontWeightBold">
                        <TextField
                          variant={'outlined'}
                          name={`post-${post?.postId}`}
                          value={posts.getUpdatePost()[`post-${post?.postId}`] || post.content}
                          onChange={posts.setUpdatePost}
                          fullWidth
                        />
                      </Box>
                      <Typography component={"p"} variant={"body2"} color="textSecondary">postId : {post?.postId}</Typography>
                    </Box>
                    <Box display="flex" mb={2}>
                      <Box mr={1}>
                        <Button
                          variant="contained"
                          color="primary"
                          onClick={() => posts.onUpdate(`${post?.postId}`)}
                        >
                          更新する
                        </Button>
                      </Box>
                      <Button
                        variant="contained"
                        color="secondary"
                        onClick={() => posts.onDelete(post?.postId)}
                      >
                        投稿を削除する
                      </Button>
                    </Box>
    
                    <Box pt={8} pb={4}><Divider /></Box>
                  </li>
                </Box>
              )
            })
          }
          </ul>
          <h2>Post - 投稿する</h2>
          <Box mt={4}>
            <TextField
              variant={'outlined'}
              value={posts.getPost()}
              onChange={(e) => posts.setPost(e.target.value)}
              fullWidth
            />
          </Box>
    
          <Box my={4}>
            <Box display="flex">
              <Box mr={2}>
                <Button
                  variant="contained"
                  color="primary"
                  onClick={() => epic.onModalEpic()}
                >
                  投稿する
                </Button>
              </Box>
    
              <Box>
                <Button
                  variant="contained"
                  color="secondary"
                  onClick={() => auth.signOut()}
                >
                  ログアウトする
                </Button>
              </Box>
            </Box>
          </Box>
        </Container>
      </Box>
    );
}

export default Home;

これでデータの取得、データの投稿、データの更新、データの削除が可能となりました。

8.Schemaの更新

GraphQLのSchemaに変更がある場合、Schemaを修正または追加し、サーバーレス環境を再デプロイする必要があります。
しかし、そのままフロントエンド側でGraphQL APIを実行するとエラーが発生します。
実際には本番環境にデプロイされていますが、変更点をAppSyncのスキーマに反映させる必要があります。

$ amplify codegen

上記のコマンドを実行すると、AppSyncからSchemaをダウンロードし、自動的にmutations.tsなどのファイルを再生成してくれます。

9.REST APIを叩く

最後に、Lambda関数をAPI Gatewayとして機能させ、REST APIとして利用してデータを取得してみたいと思います。
バックエンド編のserverless.ymlを見ると、APIにはCognitoのAuthorizerを設定しています。
このAuthorizerはCognitoの認証を完了し、認証後に取得したトークンをAPIリクエストのheadersのAuthorizationに含めることでAPIを利用することができます。
認証を行わないとAPIは常にオープンな状態でどこからでもアクセス可能になってしまうため、Authorizerを使用しています。
APIリクエストを行うためにはaxiosをインストールする必要があります。

$ npm i axios

axiosの処理ごとにトークンを含める記述をするのは手間なので、デフォルトでトークンを含めた処理を行うaxiosのインスタンスを生成するHooksを作成します。
これにより、APIリクエストを行う際にはトークンの付与を意識する必要がなくなります。

// src/hooks/useAxios.ts

import { Auth } from 'aws-amplify';
import axios, { AxiosInstance } from 'axios';

//------------------------------
// Type
//------------------------------
export type useAxiosType = {
    initAxios: () => AxiosInstance
    }
    
    //------------------------------
    // Hooks
    //------------------------------
    export const useAxios = () => {
    
    /**
     * headersにトークンをデフォルトで付ける
     */
    const initAxios = async (): Promise<AxiosInstance> => {
      const session = await Auth.currentSession()
      return axios.create({
        headers: {
          Authorization: `${session.getIdToken().getJwtToken()}`
        }
      });
    }
    
    return {
      initAxios
    }
}

次に、CustomHooksを使用して実際にエンドポイントを叩く処理を作成します。
このCustomHooksを使うことで、簡単にAPIリクエストを行うことができます。

// src/hooks/useAPI.ts

import { useAxios } from './useAxios';
import { endPoints } from '../constants/';

//------------------------------
// Type
//------------------------------
export type useAPIType = {
    fetchHello: () => Promise<any>
    }
    
    //------------------------------
    // Hooks
    //------------------------------
    export const useAPI = (): useAPIType => {
    
     const apiaxios = useAxios()
    
    /**
     * functionsのhelloをfetch
     */
    const fetchHello = async (): Promise<any> => {
      const axios = await apiaxios.initAxios()
      if (endPoints.hello) {
        const res = await axios.get(endPoints.hello)
        const data = JSON.parse(res.data.body)
        return {
          data: data
        }
      }
    }
    
    return { fetchHello }

}

それでは、実際にデータを取得してみましょう。
CustomHooksを使用して、エンドポイントにリクエストを送り、データを取得します。

// src/pages/home.tsx

useEffect(() => {
    // API Gatewayを認証有りで叩いてみる
    const init = async () => {
      await api.fetchHello().then((result) => {
        console.log(result) // data: { context, event, message}
      })
    }
    
    init()   
  // eslint-disable-next-line
},[])

APIからデータを取得することができました。
前編・後編・番外編と進めてきましたが、いかがでしたでしょうか?バックエンドの構築からフロントエンドとの接続、そして実際に動作する画面までを実現しました。

初めて触る技術では、多くの未知の要素があり、「何がわからないのかわからない」という状態になることがよくあります。
そのため、アプリケーションを実際に動かすまでの手順が具体的に存在することは非常に重要だと考えています。
インターネット上の情報は断片的なものが多く、新しい技術を学ぶ際にはそれらの情報を組み合わせて理解することは困難で大変です。
今回の記事が、Serverless Frameworkに入門する方々のお手伝いに少しでもなれば幸いです。

では、またお会いしましょう。

● 今から始めるServerless Frameworkでサーバーレスデビュー【バックエンド編#1】
https://note.com/ryoppei/n/n9163712b68ad

今から始めるServerless Frameworkでサーバーレスデビュー【フロントエンド編#2】
https://note.com/ryoppei/n/n0858cfca7784

今から始めるServerless Frameworkでサーバーレスデビュー【プラグイン番外編#3】https://note.com/ryoppei/n/ne110ac6a440f

今から始めるServerless Frameworkでサーバーレスデビュー【ファンクション番外編#4】https://note.com/ryoppei/n/n71809e2520e0

今から始めるServerless Frameworkでサーバーレスデビュー【マッピングテンプレート番外編#5】
https://note.com/ryoppei/n/n20b4afd705e5

この記事が気に入ったらサポートをしてみませんか?