Next.js 14でTodoアプリを作る – 5.CRUDを作成

投稿日:2024年03月12日

更新日:2024年03月22日

投稿者:HIROKI CHIYODA

テーブルtodosのCRUDを作成します。

CRUD とは

CRUDとはCREATE、READ、UPDATE、DELETEの頭文字をとった略称です。
データの新規作成、読み取り、更新、削除の機能の総称です。
APIを実現するためにはGET、POST、PUT、DELETEのHTTPMethodsを使います。

事前準備

ドキュメントを参考に追加します。

https://developers.cloudflare.com/d1/examples/d1-and-hono/

またドキュメント通りにしてもエラーが発生します。
エラー解決には以下の記事を参考にしました。

https://zenn.dev/da1/articles/cloudflare-nextjs-hono-drizzle

インスタンスを作成するクラスHonoのBindingsプロパティに型を指定します。
以下のように、コードを変更します。

- const app = new Hono().basePath('/api')
+ const app = new Hono<{ Bindings: Bindings }>().basePath('/api')

指定する型Bindingsを作成します。
プロジェクトのルートにtypesディレクトリを作成します。
ファイルBindings.tsを新規作成します。
以下のコードを追加します。

type Bindings = {
  DB: D1Database;
};

グローバル変数を宣言します。
ファイルenv.d.tsDB: D1Databaseを追加します。

  declare global {
    namespace NodeJS {
      interface ProcessEnv {
        // Add here the Cloudflare Bindings you want to have available in your application
        // (for more details on Bindings see: https://developers.cloudflare.com/pages/functions/bindings/)
        //
        // KV Example:
        // MY_KV: KVNamespace
+       DB: D1Database;
      }
    }
  }
  
  export {}

ついでにコードを詳しくみます。

declare global {}

このキーワードはグローバルスコープで変数を宣言するために使用されます。

namespace NodeJS {}

この名前空間はNode.js環境で使用されるグローバル変数を定義します。

interface ProcessEnv {}

このインターフェースはprocess.envオブジェクトの型を定義します。

DB: D1Database;

このプロパティはDBという名前の変数を宣言しD1Database型のオブジェクトでデータベースへの接続を表します。

1件取得

ドキュメントに1件取得するコードが載っているので、それを参考に1件取得の機能を追加します。

https://developers.cloudflare.com/d1/examples/d1-and-hono/

次にHonoでGETリクエストのためのルーティングを設定します。
以下のコードをファイルroute.tsapp.get('/hello', (c) => {})の下に追加します。

app.get('/todos/:id', async (c) => {
  const id = c.req.param('id')
  try {
    const { results } = await process.env.DB.prepare(
      'SELECT * FROM todos WHERE id = ?;'
    )
      .bind(id)
      .all()
    return c.json(results)
  } catch (e) {
    return c.json({ err: e }, 500)
  }
});

ここから追加したコードを詳しく見ていきます。

app.get('/todos/:id', async (c) => { ... });

GETリクエストを使って/todos/:idというURLにアクセスされたときにasync関数が呼び出されます。

const id = c.req.param('id')

リクエストパラメータidを取得します。
このパラメータはURLの/:id部分に指定された値になります。

const { results } = await process.env.DB.prepare(
  'SELECT * FROM todos WHERE id = ?;'
).bind(id).all();

データベースにクエリSELECT * FROM todos WHERE id = ?;を実行します。
このクエリはidが指定されたデータをtodosテーブルをすべて取得します。

return c.json(results)

クエリが成功した場合はで結果をJSON形式で返します。

return c.json({ err: e }, 500)

クエリが失敗した場合はエラーメッセージとステータスコード500を返します。

全件取得

1件取得のコードを複製して改造します。
まずはapp.get('/todos/:id'から/:idを削除します。
const id = c.req.param('id')を削除します。
次にクエリSELECT * FROM todos WHERE id = ?;からWHERE id = ?を削除します。
またbind(id)を削除します。
完成すると以下のようになります。

app.get('/todos', async (c) => {
  try {
    const { results } = await process.env.DB.prepare(
      'SELECT * FROM todos'
    ).all()
    return c.json(results)
  } catch (e) {
    return c.json({err: e}, 500)
  }
})

削除

1件取得のコードを複製して改造します。
まずはapp.getapp.deleteに変更します。
次にクエリSELECT * FROM todos WHERE id = ?;DELETE FROM todos WHERE id = ?;に変更します。
bind(id)の後のall()run()に変更します。
c.json(results)results{message:'Deleted'}に変更します。
最後にexport const DELETE = handle(app)を追加します。
完成すると以下のようになります。

app.delete('/todos/:id', async (c) => {
  const id = c.req.param('id');
  try {
    await process.env.DB.prepare('DELETE FROM todos WHERE id = ?;')
      .bind(id)
      .run()
    return c.json({ message: 'Deleted' }, 200)
  } catch (e) {
    return c.json({ err: e }, 500)
  }
})

export const DELETE = handle(app)

新規登録

ドキュメントを参考に新規登録の機能を追加します。

https://developers.cloudflare.com/d1/tutorials/build-a-comments-api/#6-insert-data

app.post('/todos', async (c) => {
  const { title, content } = await c.req.json()
  try {
    const res = await process.env.DB.prepare(
      'INSERT INTO todos (title, content) VALUES (?, ?);'
    ).bind(title, content).run()
    return c.json({ message: 'Created' }, 201)
  } catch (e) {
    return c.json({ err: e }, 500)
  }
})

更新

削除のコードを複製して改造します。
まずはapp.deleteapp.putに変更します。
const id = c.req.param("id")の下にconst { title, content } = await c.req.json()を追加します。
次にクエリDELETE FROM todos WHERE id = ?;UPDATE todos SET title = ?, content = ? WHERE id = ?;に変更します。
bind(id)idの前にtitlecontentを追加します。
c.json({message: 'Deleted'})DeletedUpdatedに変更します。
最後にexport const PUT = handle(app)を追加します。
完成すると以下のようになります。

app.put('/todos/:id', async (c) => {
  const id = c.req.param('id')
  const { title, content } = await c.req.json()
  try {
    await process.env.DB.prepare(
      'UPDATE todos SET title = ?, content = ? WHERE id = ?;'
    ).bind(title, content, id).run()
    return c.json({ message: 'Updated' })
  } catch (e) {
    return c.json({ err: e }, 500)
  }
})

export const PUT = handle(app)

動作確認

CRUD機能を実装したので、APIの動作確認をします。
簡易サーバーを立ち上げます。
以下のコマンドを実行します。

npm run pages:preview

http://localhost:8788/が立ち上がるのでAPIの動作確認をします。
動作確認には拡張機能Thunder Clientを使います。
もし導入されていないときは、拡張機能で検索して導入してください。
URLhttp://localhost:8788/api/todos/を使って以下の各メソッドを実行します。
GETメソッドを使って、全件取得、1件取得できるか確認します。
POSTメソッドを使って、新規登録できるか確認します。
PUTメソッドを使って、更新できるか確認します。
DELETEメソッドを使って、削除できるか確認します。

デプロイ

APIの動作確認ができたので、本番環境にデプロイします。
まずは本番環境のtodo-app-d1データベースにテーブルtodosを作成します。
以下のコマンドを実行します。
本番環境でテーブルを作成するときはオプションlocalをオプションremoteに変更して実行します。

npx wrangler d1 execute todo-app-d1 --remote --file=./schema.sql

本番環境のデータベースにテーブルtodosが作成されたかどう確認します。
以下のコマンドを実行します。

npx wrangler d1 execute todo-app-d1 --remote --command='SELECT * FROM todos'

プロジェクトをデプロイします。
以下のコマンドを実行します。

npm run pages:deploy

URLhttps://todo-app-d1-base.pages.dev/api/todos/を使って以下の各メソッドを実行します。
GETメソッドを使って、全件取得、1件取得できるか確認します。
POSTメソッドを使って、新規登録できるか確認します。
PUTメソッドを使って、更新できるか確認します。
DELETEメソッドを使って、削除できるか確認します。