[サンプルアプリ ハンズオン ] Next.js+Supabaseで簡単な認証とDB操作アプリを作る

# はじめに
簡単なアプリを作りながらSupabaseについて理解を深める。

:::note info
想定読者
・Supabase初心者でSupabaseについて知りたい人
・Supabaseを使ってお試しで簡単なアプリを作ってみたい人
:::

:::note warn
Next.jsの解説は含まれていません。
また、公式ドキュメントに記載があるサンプルアプリを動かしてみたという記事のため、
Supabaseを使用してバリバリコードを書いている方は対象読者から外れます。
:::


## 技術スタック
* Next.js 13
* Supabase

## 公式HP
https://nextjs.org/

https://supabase.com/


## サンプルアプリの解説
サンプルアプリでは、メール認証を行いログインをし、
ログイン後、DBに保存されたプロフィールデータを表示する。
**簡単な機能だが、認証とDBからデータの取得や更新などができるので
このアプリを改造すればいろんなWebアプリが作れるようになる。** 


サンプルアプリの動作手順は以下の通り。

**①TOP画面でメールアドレスを入力する**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/7a342c71-3297-d49c-c6aa-8e72328e0995.png)

**②ログインメールが届く**
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/8c637405-05b0-1f5e-8def-fcbbfeccce33.png">

**③Log inをクリックでユーザページが開き、DBのデータ(プロフィール情報)が表示される**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/3ab79d98-2be2-6a2e-42c3-c0130d3ac26c.png)

SupabaseのDB管理画面を確認すると、ブラウザに表示されていたデータがテーブルに格納されている。
そのため、ログアウトして再度ログインしても同じ情報を表示することができる。
また、ブラウザ画面で情報を変更して「Update」ボタンを押すとデータがちゃんと更新される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/622cc0f9-8234-fced-275c-0238e2357e67.png)


# Supabaseとは
Supabaseは**BaaS(Backend as a Service)** の一種でバックエンド(アプリの裏側)の機能を提供してくれるサービスです。
BaaSを使うとバックエンドで必要な機能を開発することなく利用することができるので、アプリ開発が早くなるというメリットがあります。

Supabaseは以下の機能を提供しています。
**1.Database (データベース)**
**2.Auth (認証)**
3.Storage (データ保存(画像など))
4.AI&Vector (AI)
5.Realtime
6.Edge Function

今回のサンプルアプリは1と２を使います。4~6は具体的にどんな機能なのかよく分かっていません。。

# サンプルアプリのハンズオン
ここからサンプルアプリを作ります。
参考にしたサイトは以下になります。

https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs#project-setup

## プロジェクトを作成する
まずはSupabaseでプロジェクトを作成します。
公式HPにアクセスして「Start your project」をクリック。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/f78c9ff3-b67e-0836-ca50-f5f402795edf.png">

アカウントを作成していない人はまず、アカウント作成から始まります。
私はGitHubアカウントを持っているので「Continue with GitHub」をクリック。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/5f708150-80c7-498e-9a0b-0ebeb23bc8c0.png">

ボタンをポチポチしていくとアカウントが作成されます。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/2e19b031-a2eb-e933-2d9a-7da6788a9f23.png">

アカウントが作成されるとダッシュボード画面に遷移します。
そこで「New Project」をクリック
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/2eb89e9e-3e50-c8b5-11a9-a9bff42fc5d1.png">

プロジェクト作成画面にいくので
・Name
・Database Password
・Region
・Pricing Plan
を入力して右下の「Create New Project」をクリック。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/f74c180c-aa85-9e74-ce9f-224f181fa93c.png">

そしたらプロジェクトが作成されて「Welcome to your new project」ページに遷移します。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/2b0bff7d-5a65-623a-dd30-e970a6c10083.png">

## データベースを用意する
ダッシュボード画面で左にあるアイコンから「SQL Editer」を選択し、
「Uer Management Starter」をクリックします。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/77997474-9dc4-52b2-0587-b8aec836efe5.png">


profilesテーブルを作成するSQL文が最初から用意されているので何も変更せずに
右下の「RUN」をクリックします。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/f19ea521-b0d3-440c-b635-b5b9c4d3e673.png">


## API Keyを確認する
ホーム画面に戻り、「View API settings」をクリックします。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/b677c91e-3b3c-16dd-ccaa-4c685473c73d.png">

API Settingsが確認できます。この画面のURLとanon Keyは後でプログラムに書き込みます。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/fdcf07b9-6020-f255-bbad-96159b47a0f1.png">


## 新規プロジェクトを作成する(Next.jsの準備)
VS Codeを開いてプロジェクトを作成したいフォルダに移動します。
私はドキュメント直下に「Supabase」というフォルダを作成し、その中にプロジェクトを作成することにしました。

そのため、VS Codeで「Supabase」フォルダを開いた状態でVSCodeのターミナルに以下のコマンドを入力します。末尾の「supabase-nextjs」は作成するプロジェクトの名前のため好きな名前を設定して良いですが、私は公式ドキュメントの例をそのまま使いました。
```
npx create-next-app@latest --use-npm supabase-nextjs
```

コマンドを入力してエンターを押すと以下のように何問か質問を聞かれます。
(例えば言語を「javaScript」にするか「TypeScript」にするかなど。)
ここはデフォルトの設定のままエンター連打で問題ないですが、私は「TypeScript」を選択してみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/dcea9f32-39ec-8af0-ea01-d99cc88564aa.png">


Nextjsアプリの構築方法や質問も意味などを知りたい方は、この方の記事が参考になると思います。

https://qiita.com/kaba_san/items/302b8b731e7fbfa56367


少し待つとプロジェクトが自動で作成され、「Success！」と表示され、準備ができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/b644baf0-9bcf-d7fa-81fd-7eadf211d7ea.png">

プロジェクトが作成されたら以下のコマンドでプロジェクトの中に移動する。
```
cd supabase-nextjs
```

次に「supabase-js」というライブラリをインストールするために以下のコマンドを実行します。
```
npm install @supabase/supabase-js
```

次に「.env.local」ファイルをsupabase-nextjsフォルダ直下に新規で作成します。
その「.env.local」ファイル内で
・NEXT_PUBLIC_SUPABASE_URL
・NEXT_PUBLIC_SUPABASE_ANON_KEY
を設定します。
ここに先ほど確認したURLとAnon　Keyを以下のように入力します。
```
NEXT_PUBLIC_SUPABASE_URL=先ほど確認したURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=先ほど確認したanon Key
```

こんな感じです。(URLとanonKeyは隠しています。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/b94a96a0-8e32-11ed-0718-e9006ca1a203.png)


次に以下のコマンドを入力してauth-helpers-nextjsライブラリをインストールします。
```
npm install @supabase/auth-helpers-nextjs
```

## middlewareを設定する
middlewareとはリダイレクトをしたり認証をできるようにしてくれるライブラリ？です。
とりあえず認証がしたいときは必要そうです。(自信なくてすみません。)

公式HPは以下になります。

https://nextjs.org/docs/pages/building-your-application/routing/middleware

また、この方の記事が分かりやすかったです。

https://zenn.dev/kiriyama/articles/b0d6f8b2362107



supabase-nextjsフォルダ直下に「middleware.ts」ファイルを新規作成し、
以下のコードをコピーします。

```TypeScript
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'

import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  const res = NextResponse.next()
  const supabase = createMiddlewareClient({ req, res })

  const {
    data: { user },
  } = await supabase.auth.getUser()

  // if user is signed in and the current path is / redirect the user to /account
  if (user && req.nextUrl.pathname === '/') {
    return NextResponse.redirect(new URL('/account', req.url))
  }

  // if user is not signed in and the current path is not / redirect the user to /
  if (!user && req.nextUrl.pathname !== '/') {
    return NextResponse.redirect(new URL('/', req.url))
  }

  return res
}

export const config = {
  matcher: ['/', '/account'],
}
```

次に、auth-ui-reactライブラリとauth-ui-sharedをインストールするために、ターミナルで以下のコマンドを入力します。
```
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
```

ここからは認証機能を作るためにたくさんファイルを作成します。
全部公式ホームページからコピペですが以下の通りです。

supabase-nextjsフォルダ直下にappフォルダを新規作成
appフォルダの直下に「auth-form.tsx」を作成。コードは以下の通り。
```TypeScript
'use client'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { Database } from './database.types'

export default function AuthForm() {
  const supabase = createClientComponentClient<Database>()

  return (
    <Auth
      supabaseClient={supabase}
      view="magic_link"
      appearance={{ theme: ThemeSupa }}
      theme="dark"
      showLinks={false}
      providers={[]}
      redirectTo="http://localhost:3000/auth/callback"
    />
  )
}
```

appフォルダ直下に「page.js」ファイルを新規作成。コードは以下の通り
```TypeScript
import AuthForm from './auth-form'

export default function Home() {
  return (
    <div className="row">
      <div className="col-6">
        <h1 className="header">Supabase Auth + Storage</h1>
        <p className="">
          Experience our Auth and Storage through a simple profile management example. Create a user
          profile and upload an avatar image. Fast, simple, secure.
        </p>
      </div>
      <div className="col-6 auth-widget">
        <AuthForm />
      </div>
    </div>
  )
}
```

appフォルダ直下にauthフォルダを新規作成。
authフォルダ直下にcallbackフォルダを新規作成。
callbackフォルダ直下にroute.tsファイルを新規作成。コードは以下の通り。
```TypeScript
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  const supabase = createRouteHandlerClient({ cookies })
  const { searchParams } = new URL(req.url)
  const code = searchParams.get('code')

  if (code) {
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/account', req.url))
}
```

appフォルダ直下のauthフォルダ直下にsignoutフォルダを新規作成。
signoutフォルダ直下にroute.jsファイルを新規作成。コードは以下の通り。
```TypeScript
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'

export async function POST(req) {
  const supabase = createRouteHandlerClient({ cookies })

  // Check if we have a session
  const {
    data: { session },
  } = await supabase.auth.getSession()

  if (session) {
    await supabase.auth.signOut()
  }

  return NextResponse.redirect(new URL('/', req.url), {
    status: 302,
  })
}
```

appフォルダ直下にaccountフォルダを新規作成。
accountフォルダ直下にaccount-form.tsxファイルを新規作成。コードは以下の通り
```TypeScript
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Database } from '../database.types'
import { Session, createClientComponentClient } from '@supabase/auth-helpers-nextjs'

export default function AccountForm({ session }: { session: Session | null }) {
  const supabase = createClientComponentClient<Database>()
  const [loading, setLoading] = useState(true)
  const [fullname, setFullname] = useState<string | null>(null)
  const [username, setUsername] = useState<string | null>(null)
  const [website, setWebsite] = useState<string | null>(null)
  const [avatar_url, setAvatarUrl] = useState<string | null>(null)
  const user = session?.user

  const getProfile = useCallback(async () => {
    try {
      setLoading(true)

      let { data, error, status } = await supabase
        .from('profiles')
        .select(`full_name, username, website, avatar_url`)
        .eq('id', user?.id)
        .single()

      if (error && status !== 406) {
        throw error
      }

      if (data) {
        setFullname(data.full_name)
        setUsername(data.username)
        setWebsite(data.website)
        setAvatarUrl(data.avatar_url)
      }
    } catch (error) {
      alert('Error loading user data!')
    } finally {
      setLoading(false)
    }
  }, [user, supabase])

  useEffect(() => {
    getProfile()
  }, [user, getProfile])

  async function updateProfile({
    username,
    website,
    avatar_url,
  }: {
    username: string | null
    fullname: string | null
    website: string | null
    avatar_url: string | null
  }) {
    try {
      setLoading(true)

      let { error } = await supabase.from('profiles').upsert({
        id: user?.id as string,
        full_name: fullname,
        username,
        website,
        avatar_url,
        updated_at: new Date().toISOString(),
      })
      if (error) throw error
      alert('Profile updated!')
    } catch (error) {
      alert('Error updating the data!')
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="form-widget">
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session?.user.email} disabled />
      </div>
      <div>
        <label htmlFor="fullName">Full Name</label>
        <input
          id="fullName"
          type="text"
          value={fullname || ''}
          onChange={(e) => setFullname(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          type="text"
          value={username || ''}
          onChange={(e) => setUsername(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="website">Website</label>
        <input
          id="website"
          type="url"
          value={website || ''}
          onChange={(e) => setWebsite(e.target.value)}
        />
      </div>

      <div>
        <button
          className="button primary block"
          onClick={() => updateProfile({ fullname, username, website, avatar_url })}
          disabled={loading}
        >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>

      <div>
        <form action="/auth/signout" method="post">
          <button className="button block" type="submit">
            Sign out
          </button>
        </form>
      </div>
    </div>
  )
}
```

appフォルダ直下のaccountフォルダ直下にpage.tsxファイルを新規作成。コードは以下の通り。
```TypeScript
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { Database } from '../database.types'
import AccountForm from './account-form'

export default async function Account() {
  const supabase = createServerComponentClient<Database>({ cookies })

  const {
    data: { session },
  } = await supabase.auth.getSession()

  return <AccountForm session={session} />
}
```

これでサンプルアプリは完成！
ターミナルで以下のコマンドを実行するとアプリが起動する
```
npm run dev
```

ちなみにここまでの手順は上記で紹介した公式HPに全部書いてあるが
私は記載通りにここまで試したが、エラーが出てしまった。
具体期には以下のエラーが出た。
```
Parsing error: Cannot find module 'next/babel'
```
ちょっと分からなくでChatGPTちゃんに聞いたところ、バージョンの互換性の問題のようで、
以下の指示通りに実行したらエラーが消えてくれた。
もし同様のエラーが出た際は実施をしてみてほしい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322871/a17d5101-3669-1007-6b71-ed156a93acee.png)


# まとめ
SupabaseはBaaSの一種で認証やDBなどの機能を提供してくれる。
Next.jsやVueなどさまざまなフレームワークに対応している。
使うためにはSupabaseのアカウント作成、プロジェクト作成が必要だが簡単にできる。
ソースコードにライブラリを入れるときもコマンドを入力すれば簡単にインストールできる。

# 最後に
簡単にバックエンド側の処理が実装できて、確かに開発効率が上がると感じた。
サンプルコードを動かせたのはいいが、理解を深めるためにアプリを自作したい！


# 参考資料
私がこの記事を書くために参考にした資料を紹介します。
私の記事は読まなくても、以下の資料を確認すればきっとめちゃくちゃ理解できます！

## プログラミングチュートリアルさんのyoutube動画
この方の動画はどれも分かりやすいです。Udemy講座も多く公開していますが、どれも分かりやすいです。

https://www.youtube.com/watch?v=CZlZgRo0bZ4&t=1630s

## Qiita記事 
### create-next-appを用いたNextjsアプリ構築方法
create-next-appを用いたNextjsアプリ構築方法が丁寧に書いています。

https://qiita.com/kaba_san/items/302b8b731e7fbfa56367

### 飛躍的に伸びているBaaS「Supabase」の概要と所感
Supabaseについて分かりやすくまとめられています。

https://qiita.com/kabochapo/items/6a2a391832825d17af7d