設計

Next.js 16でTursoとDrizzle ORMを導入するステップバイステップガイド

Next.jsプロジェクトにTurso(エッジSQLiteデータベース)とDrizzle ORMを導入し、型安全なデータベース操作を実現する方法を詳しく解説

Next.js でデータベースを扱う際、どの ORM とデータベースを選ぶかは重要な決定です。本記事では、Turso(エッジ最適化された SQLite データベース)と Drizzle ORM の組み合わせを推奨し、その導入方法をステップバイステップで解説します。

なぜ Turso + Drizzle ORM なのか

Turso とは

Turso は libSQL をベースにしたエッジ最適化された SQLite データベースサービスです。

  • グローバル分散: エッジロケーションにデータをレプリケート
  • 低レイテンシ: ユーザーに近い場所からデータを提供
  • SQLite 互換: 使い慣れた SQL 構文をそのまま使用可能
  • Embedded Replicas: ローカルにレプリカを持ち、マイクロ秒レベルの読み取りを実現
  • 無料枠が充実: 個人プロジェクトや小規模アプリに最適

Drizzle ORM を選ぶ理由

Turso と組み合わせる ORM として、Drizzle ORM を強く推奨します。

特徴Drizzle ORMPrisma
Turso サポートネイティブサポートアダプター経由
バンドルサイズ軽量(〜50KB)大きい(〜2MB)
Edge Runtime完全対応制限あり
型安全性TypeScript ファースト生成された型
マイグレーションDrizzle Kit 内蔵Prisma Migrate
クエリ構文SQL に近い独自 DSL

Drizzle ORM は Turso/libSQL にネイティブ対応しており、Edge Runtime での動作も優れています。

ステップ1: Turso CLI のインストールとセットアップ

Turso CLI のインストール

# macOS (Homebrew)
brew install tursodatabase/tap/turso

# Linux / WSL
curl -sSfL https://get.tur.so/install.sh | bash

# Windows (PowerShell)
iwr -useb https://get.tur.so/install.ps1 | iex

Turso にログイン

turso auth login

ブラウザが開き、GitHub アカウントでの認証が求められます。

データベースの作成

# データベースを作成(名前は任意)
turso db create my-nextjs-app

# 作成されたデータベースの確認
turso db list

認証情報の取得

# データベース URL の取得
turso db show --url my-nextjs-app

# 認証トークンの作成
turso db tokens create my-nextjs-app

出力された URL とトークンを控えておいてください。

ステップ2: プロジェクトへのパッケージインストール

必要なパッケージのインストール

# npm
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit

# pnpm
pnpm add drizzle-orm @libsql/client
pnpm add -D drizzle-kit

# bun
bun add drizzle-orm @libsql/client
bun add -D drizzle-kit

環境変数の設定

.env.local ファイルを作成(または編集)します:

# Turso Database
TURSO_DATABASE_URL=libsql://your-database-org.turso.io
TURSO_AUTH_TOKEN=your-auth-token-here

注意: .env.local.gitignore に追加してください。

ステップ3: データベース接続の設定

libSQL クライアントの設定

src/db/index.ts を作成します:

import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";

const tursoUrl = process.env.TURSO_DATABASE_URL;
const tursoAuthToken = process.env.TURSO_AUTH_TOKEN;

if (!tursoUrl || !tursoAuthToken) {
  throw new Error("TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set.");
}

const client = createClient({
  url: tursoUrl,
  authToken: tursoAuthToken,
});

export const db = drizzle(client, { schema });

Embedded Replicas を使用する場合(オプション)

ローカルにレプリカを持ち、読み取りを高速化する設定です:

import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import * as schema from "./schema";

const client = createClient({
  url: "file:local-replica.db", // ローカルファイル
  syncUrl: process.env.TURSO_DATABASE_URL,
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60, // 60秒ごとに自動同期
});

export const db = drizzle(client, { schema });

// 手動で同期する場合
export async function syncDatabase() {
  await client.sync();
}

ステップ4: スキーマの定義

基本的なスキーマファイル

src/db/schema.ts を作成します:

import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

// ユーザーテーブル
export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: text("created_at")
    .notNull()
    .default(sql`(datetime('now'))`),
  updatedAt: text("updated_at")
    .notNull()
    .default(sql`(datetime('now'))`),
});

// 投稿テーブル
export const posts = sqliteTable("posts", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  title: text("title").notNull(),
  content: text("content"),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id),
  published: integer("published", { mode: "boolean" }).default(false),
  createdAt: text("created_at")
    .notNull()
    .default(sql`(datetime('now'))`),
});

// 型のエクスポート
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

リレーションの定義

src/db/relations.ts を作成します:

import { relations } from "drizzle-orm";
import { users, posts } from "./schema";

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

スキーマファイルでリレーションをエクスポートします:

// src/db/schema.ts に追加
export * from "./relations";

ステップ5: Drizzle Kit の設定

設定ファイルの作成

プロジェクトルートに drizzle.config.ts を作成します:

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "turso",
  dbCredentials: {
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN,
  },
});

package.json にスクリプトを追加

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio"
  }
}

ステップ6: マイグレーションの実行

マイグレーションファイルの生成と実行

# マイグレーションファイルを生成
npm run db:generate

# マイグレーションを実行
npm run db:migrate

生成されたマイグレーションファイルは drizzle/ ディレクトリに保存されます。

Drizzle Studio でデータを確認

npm run db:studio

ブラウザでデータベースの内容を GUI で確認・編集できます。

ステップ7: Next.js での使用例

App Router でのデータ取得

src/app/users/page.tsx:

import { db } from "@/db";
import { users } from "@/db/schema";

export default async function UsersPage() {
  const allUsers = await db.select().from(users);

  return (
    <div>
      <h1>ユーザー一覧</h1>
      <ul>
        {allUsers.map((user) => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
}

リレーションを含むクエリ

import { db } from "@/db";
import { posts, users } from "@/db/schema";
import { eq } from "drizzle-orm";

export default async function PostsPage() {
  // リレーションクエリを使用
  const postsWithAuthors = await db.query.posts.findMany({
    with: {
      author: true,
    },
  });

  // または JOIN を使用
  const result = await db
    .select({
      post: posts,
      author: users,
    })
    .from(posts)
    .leftJoin(users, eq(posts.authorId, users.id));

  return (
    <div>
      <h1>投稿一覧</h1>
      {postsWithAuthors.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>著者: {post.author.name}</p>
        </article>
      ))}
    </div>
  );
}

Server Actions でのデータ操作

src/app/users/actions.ts:

"use server";

import { db } from "@/db";
import { users, type NewUser } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function createUser(data: NewUser) {
  const result = await db.insert(users).values(data).returning();
  revalidatePath("/users");
  return result[0];
}

export async function updateUser(id: number, data: Partial<NewUser>) {
  const result = await db
    .update(users)
    .set(data)
    .where(eq(users.id, id))
    .returning();
  revalidatePath("/users");
  return result[0];
}

export async function deleteUser(id: number) {
  await db.delete(users).where(eq(users.id, id));
  revalidatePath("/users");
}

API Route での使用

src/app/api/users/route.ts:

import { db } from "@/db";
import { users } from "@/db/schema";
import { NextResponse } from "next/server";

export async function GET() {
  const allUsers = await db.select().from(users);
  return NextResponse.json(allUsers);
}

export async function POST(request: Request) {
  const body = await request.json();

  const newUser = await db
    .insert(users)
    .values({
      name: body.name,
      email: body.email,
    })
    .returning();

  return NextResponse.json(newUser[0], { status: 201 });
}

ステップ8: Edge Runtime での使用

Edge Function の設定

src/app/api/edge-users/route.ts:

import { db } from "@/db";
import { users } from "@/db/schema";
import { NextResponse } from "next/server";

export const runtime = "edge";

export async function GET() {
  const allUsers = await db.select().from(users);
  return NextResponse.json(allUsers);
}

Turso + Drizzle の組み合わせは Edge Runtime で完全に動作します。

ステップ9: 高度なクエリパターン

フィルタリングと並び替え

import { db } from "@/db";
import { posts } from "@/db/schema";
import { eq, and, or, desc, like } from "drizzle-orm";

// 公開済みの投稿を新しい順に取得
const publishedPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt));

// 複合条件
const filteredPosts = await db
  .select()
  .from(posts)
  .where(
    and(
      eq(posts.published, true),
      or(
        like(posts.title, "%Next.js%"),
        like(posts.content, "%TypeScript%")
      )
    )
  );

ページネーション

import { db } from "@/db";
import { posts } from "@/db/schema";
import { desc } from "drizzle-orm";

async function getPaginatedPosts(page: number, pageSize: number = 10) {
  const offset = (page - 1) * pageSize;

  const results = await db
    .select()
    .from(posts)
    .orderBy(desc(posts.createdAt))
    .limit(pageSize)
    .offset(offset);

  return results;
}

トランザクション

import { db } from "@/db";
import { users, posts } from "@/db/schema";

async function createUserWithPost(
  userData: NewUser,
  postData: Omit<NewPost, "authorId">
) {
  return await db.transaction(async (tx) => {
    const [user] = await tx.insert(users).values(userData).returning();

    const [post] = await tx
      .insert(posts)
      .values({
        ...postData,
        authorId: user.id,
      })
      .returning();

    return { user, post };
  });
}

プロジェクト構成のまとめ

src/
├── app/
│   ├── api/
│   │   └── users/
│   │       └── route.ts
│   ├── users/
│   │   ├── page.tsx
│   │   └── actions.ts
│   └── layout.tsx
├── db/
│   ├── index.ts          # DB接続設定
│   ├── schema.ts         # テーブル定義
│   └── relations.ts      # リレーション定義
drizzle/
├── 0000_*.sql            # マイグレーションファイル
└── meta/
drizzle.config.ts         # Drizzle Kit 設定
.env.local                # 環境変数

トラブルシューティング

接続エラーが発生する

環境変数が正しく設定されているか確認してください:

# 環境変数の確認
echo $TURSO_DATABASE_URL
echo $TURSO_AUTH_TOKEN

マイグレーションが失敗する

Turso のデータベースに接続できるか確認:

turso db shell my-nextjs-app

Edge Runtime でエラーが発生する

@libsql/client の Web 互換バージョンを使用していることを確認してください。最新版では自動的に適切なバージョンが選択されます。

まとめ

Turso + Drizzle ORM の組み合わせは、Next.js プロジェクトに最適なデータベースソリューションです:

  1. Turso: エッジ最適化された SQLite で低レイテンシを実現
  2. Drizzle ORM: 軽量で型安全、Turso にネイティブ対応
  3. Edge Runtime 対応: Vercel Edge Functions などで完全動作
  4. 開発体験: Drizzle Studio による GUI でのデータ管理

この組み合わせにより、型安全で高速なデータベース操作を実現できます。