Try   HackMD

使用 Cloudflare Pages 建立 Serverless Next.js API (with D1 database)

前言

Cloudflare pages 可以根據 github commit 建立每個 Pull Request 的 staging 網頁

截圖 2024-01-06 下午4.42.38

有這樣的 staging deployment 可以有利於 PR review 及 debug。

我們也可以用 Cloudflare 提供的 pages/workers 建立 serverless framework 處理 API,省去架設伺服器的困難及成本。

並用 D1 database 儲存資料,避免 serverless 狀態被清除。

文件

部署 Next.js site

參考 Pages: Deploy a Next.js site

  1. Create 一個 next.js project

    ​​​npm create cloudflare@latest nextjs-pages-example -- --framework=next
    

    設定 config
    接著進入資料夾

    ​​​cd nextjs-pages-example
    

    原本應該就有個 default 的 api/hello

  2. 上傳到 Github

    ​​​git remote add origin https://github.com/<GITHUB_USER_NAME>/nextjs-pages-example.git
    ​​​git branch -M main
    ​​​git push -u origin main
    
  3. 到 Cloudflare Workers 和 Pages
    建立應用程式

    截圖 2024-01-06 下午5.04.31
    選擇 Pages 的 tab,並選連線至 Git
    截圖 2024-01-06 下午5.04.58

    選擇 project 的 repository
    並按 開始設定
    選擇 Next.js 的設定
    截圖 2024-01-06 下午5.06.34

    進到 設定 中的 函數
    截圖 2024-01-06 下午5.10.24

    選擇相容性日期: 2022-11-30 (生產跟預覽)
    截圖 2024-01-06 下午5.11.08

    設定相容性旗標: nodejs_compat (生產跟預覽)
    截圖 2024-01-06 下午5.13.26

    再部署一次
    截圖 2024-01-06 下午5.13.59

  4. 可以在新的 url 下呼叫 api/hello
    e.g. https://c030509d.nextjs-pages-example.pages.dev/api/hello

建立 D1 database

參考 D1: Get Started

  1. 登入 Cloudflare
    ​​​npx wrangler login
    
  2. 新增 database 名稱為 prod-d1-tutorial
    ​​​npx wrangler d1 create prod-d1-tutorial
    
    如果成功會出現 d1 資訊
    ​​​[[d1_databases]]
    ​​​binding = "DB" # available in your Worker on env.DB
    ​​​database_name = "prod-d1-tutorial"
    ​​​database_id = "<unique-ID-for-your-database>"
    
  3. 在 root 建立一個 wrangler.toml 檔案
    ​​​[[d1_databases]]
    ​​​binding = "DB" # available in your Worker on env.DB
    ​​​database_name = "prod-d1-tutorial"
    ​​​database_id = "<unique-ID-for-your-database>"
    
  4. 在 root 建立一個 schema.sql 檔案
    ​​​DROP TABLE IF EXISTS Customers;
    ​​​CREATE TABLE IF NOT EXISTS Customers (CustomerId INTEGER PRIMARY KEY, CompanyName TEXT, ContactName TEXT);
    ​​​INSERT INTO Customers (CustomerID, CompanyName, ContactName) VALUES (1, 'Alfreds Futterkiste', 'Maria Anders'), (4, 'Around the Horn', 'Thomas Hardy'), (11, 'Bs Beverages', 'Victoria Ashworth'), (13, 'Bs Beverages', 'Random Name');
    
  5. 執行此 sql 指令
    ​​​npx wrangler d1 execute prod-d1-tutorial --local --file=./schema.sql
    
    若帶上 --local 的 flag 則表示只在 local 執行,不會變更到 Cloudflare D1 裡的資料
    並會在 root 資料夾新增一個 .wrangler 資料夾
    儲存 D1 的資料
  6. 檢查是否儲存成功
    ​​​npx wrangler d1 execute prod-d1-tutorial --local --command="SELECT * FROM Customers"
    
    會回傳
    ​​​🌀 Mapping SQL input into an array of statements
    ​​​🌀 Executing on local database prod-d1-tutorial (21c8234f-e616-4128-93bc-b65ff78b73f5) from .wrangler/state/v3/d1:
    ​​​┌────────────┬─────────────────────┬───────────────────┐
    ​​​│ CustomerId │ CompanyName         │ ContactName       │
    ​​​├────────────┼─────────────────────┼───────────────────┤
    ​​​│ 1          │ Alfreds Futterkiste │ Maria Anders      │
    ​​​├────────────┼─────────────────────┼───────────────────┤
    ​​​│ 4          │ Around the Horn     │ Thomas Hardy      │
    ​​​├────────────┼─────────────────────┼───────────────────┤
    ​​​│ 11         │ Bs Beverages        │ Victoria Ashworth │
    ​​​├────────────┼─────────────────────┼───────────────────┤
    ​​​│ 13         │ Bs Beverages        │ Random Name       │
    ​​​└────────────┴─────────────────────┴───────────────────┘
    
    
  7. 若要上傳此 data 到 D1 則把 --local flag 去掉

建立呼叫 D1 的 API

  1. 在 root 建立一個 env.d.ts 的檔案

    參考 Pages: Use bindings in your Next.js application

    ​​​declare global {
    ​​​    namespace NodeJS {
    ​​​        interface ProcessEnv {
    ​​​            [key: string]: string | undefined;
    ​​​            DB: D1Database;
    ​​​        }
    ​​​    }
    ​​​}
    
    ​​​export { };
    
  2. 建立一個 src/app/api/db/route.ts 的檔案
    回傳 DB 中的資料
    ​​​import type { NextRequest } from 'next/server'
    
    ​​​export const runtime = 'edge'
    
    ​​​export async function GET(request: NextRequest) {
    ​​​    const { results } = await process.env.DB.prepare(
    ​​​        "SELECT * FROM Customers WHERE CompanyName = ?"
    ​​​    )
    ​​​     .bind("Bs Beverages")
    ​​​     .all();
    ​​​    return new Response(JSON.stringify(results));
    ​​​}
    
  3. 設定 D1 資料庫繫結 DB = prod-d1-tutorial(生產和預覽)
    截圖 2024-01-06 下午5.47.38
  4. 此時 push code 到 github,就能看到新 deploy 的 pages 有 api/db 回傳 D1 的資料。 e.g. https://c97ea8ef.nextjs-pages-example.pages.dev/api/db

如何在 local 測試 D1 pages

參考 Pages: Interact with your D1 databases locally

  1. 用在 cloudflare 的設定 build:

    ​​​npx @cloudflare/next-on-pages@1
    

    ⚠️ 每次更動 router 都必須執行此指定一次。
    也必須重開 server

  2. npx wrangler pages dev 啟動 server,並設定以下 flags

    ​​​npx wrangler pages dev .vercel/output/static \
    ​​​    --compatibility-date 2022-11-30 \
    ​​​    --compatibility-flag nodejs_compat \
    ​​​    --d1 DB=<unique-ID-for-your-database>
    

    e.g. 我的 id = 21c8234f-e616-4128-93bc-b65ff78b73f5

    ​​​npx wrangler pages dev .vercel/output/static \
    ​​​    --compatibility-date 2022-11-30 \
    ​​​    --compatibility-flag nodejs_compat \
    ​​​    --d1 DB=21c8234f-e616-4128-93bc-b65ff78b73f5
    

    雖然不是真的跟 Cloudflare的 D1 互動,但是還是要用 cloudflare 上的 db id

成果

Github: https://github.com/vivianjeng/nextjs-pages-example
Pages link: https://nextjs-pages-example.pages.dev