appwrite-typescript について
TypeScript 開発者向けの Appwrite クライアント・サーバー SDK ガイドです。認証、テーブルデータベース、ストレージ、リアルタイム購読、関数、アクセス制御を扱います。
主な機能
- 01Web、React Native、サーバー SDK の設定
- 02認証と SSR セッションのパターン
- 03TablesDB のクエリと型付き行データ
- 04ファイルストレージとリアルタイム購読
- 05関数、チーム、権限の管理
活用シーン
- 01アプリに Appwrite 認証を組み込む
- 02TypeScript で型付きテーブル操作を構築する
- 03ファイルをアップロードしリソース変更を購読する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ appwrite/ skills --skill appwrite-typescriptインストール後、Claude Code で/appwrite-typescriptを入力して読み込まれたか確認します。
詳細
Appwrite TypeScript SDK
インストール
# Web
npm install appwrite
# React Native
npm install react-native-appwrite
# Node.js / Deno
npm install node-appwrite
クライアントのセットアップ
クライアントサイド (Web / React Native)
// Web
import { Client, Account, TablesDB, Storage, ID, Query } from 'appwrite';
// React Native
import { Client, Account, TablesDB, Storage, ID, Query } from 'react-native-appwrite';
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
サーバーサイド (Node.js / Deno)
import { Client, Users, TablesDB, Storage, Functions, ID, Query } from 'node-appwrite';
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject(process.env.APPWRITE_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
コード例
認証 (クライアントサイド)
const account = new Account(client);
// Email signup
await account.create({
userId: ID.unique(),
email: '[email protected]',
password: 'password123',
name: 'User Name'
});
// Email login
const session = await account.createEmailPasswordSession({
email: '[email protected]',
password: 'password123'
});
// OAuth login (Web)
account.createOAuth2Session({
provider: OAuthProvider.Github,
success: 'https://example.com/success',
failure: 'https://example.com/fail',
scopes: ['repo', 'user'] // optional — provider-specific scopes
});
// Get current user
const user = await account.get();
// Logout
await account.deleteSession({ sessionId: 'current' });
OAuth 2 ログイン (React Native)
重要:
createOAuth2Session()は React Native では動作しません。代わりにcreateOAuth2Token()をディープリンクと組み合わせて使用する必要があります。
セットアップ
必要な依存関係をインストールします:
npx expo install react-native-appwrite react-native-url-polyfill
npm install expo-auth-session expo-web-browser expo-linking
app.json に URL スキームを設定します:
{
"expo": {
"scheme": "appwrite-callback-[PROJECT_ID]"
}
}
OAuth フロー
import { Client, Account, OAuthProvider } from 'react-native-appwrite';
import { makeRedirectUri } from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser';
const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
const account = new Account(client);
async function oauthLogin(provider: OAuthProvider) {
// Create deep link that works across Expo environments
const deepLink = new URL(makeRedirectUri({ preferLocalhost: true }));
const scheme = `${deepLink.protocol}//`; // e.g. 'exp://' or 'appwrite-callback-[PROJECT_ID]://'
// Get the OAuth login URL
const loginUrl = await account.createOAuth2Token({
provider,
success: `${deepLink}`,
failure: `${deepLink}`,
});
// Open browser and listen for the scheme redirect
const result = await WebBrowser.openAuthSessionAsync(`${loginUrl}`, scheme);
if (result.type !== 'success') return;
// Extract credentials from the redirect URL
const url = new URL(result.url);
const secret = url.searchParams.get('secret');
const userId = url.searchParams.get('userId');
// Create session with the OAuth credentials
await account.createSession({ userId, secret });
}
// Usage
await oauthLogin(OAuthProvider.Github);
await oauthLogin(OAuthProvider.Google);
ユーザー管理 (サーバーサイド)
const users = new Users(client);
// Create user
const user = await users.create({
userId: ID.unique(),
email: '[email protected]',
password: 'password123',
name: 'User Name'
});
// List users
const list = await users.list({ queries: [Query.limit(25)] });
// Get user
const fetched = await users.get({ userId: '[USER_ID]' });
// Delete user
await users.delete({ userId: '[USER_ID]' });
データベース操作
注意: すべての新しいコードでは
TablesDB(Databasesクラスは非推奨)を使用してください。既存のコードベースがすでに依存している場合、またはユーザーが明示的に要求した場合にのみDatabasesを使用してください。ヒント: すべての SDK メソッド呼び出しでは、オブジェクトパラメータ形式(例:
{ databaseId: '...' })を推奨します。既存のコードベースがすでに位置引数を使用している場合、またはユーザーが明示的に要求した場合にのみ位置引数を使用してください。
const tablesDB = new TablesDB(client);
// Create database (server-side only)
const db = await tablesDB.create({ databaseId: ID.unique(), name: 'My Database' });
// Create table (server-side only)
const col = await tablesDB.createTable({
databaseId: '[DATABASE_ID]',
tableId: ID.unique(),
name: 'My Table'
});
// Create row
const doc = await tablesDB.createRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: { title: 'Hello World', content: 'Example content' }
});
// List rows with query
const results = await tablesDB.listRows({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('status', 'active'), Query.limit(10)]
});
// Get row
const row = await tablesDB.getRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]'
});
// Update row
await tablesDB.updateRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
data: { title: 'Updated Title' }
});
// Delete row
await tablesDB.deleteRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]'
});
文字列カラム型
注意: レガシーの
string型は非推奨です。すべての新しいカラムには明示的なカラム型を使用してください。
| 型 | 最大文字数 | インデックス | ストレージ |
|---|---|---|---|
varchar | 16,383 | 完全インデックス (サイズが 768 以下の場合) | 行内にインライン |
text | 16,383 | プレフィックスのみ | オフページ |
mediumtext | 4,194,303 | プレフィックスのみ | オフページ |
longtext | 1,073,741,823 | プレフィックスのみ | オフページ |
varcharはインラインで保存され、64 KB の行サイズ制限にカウントされます。名前、スラッグ、識別子など、短くインデックスされたフィールドに適しています。text、mediumtext、longtextはオフページに保存されるため(行内には 20 バイトのポインタのみが存在します)、行サイズの予算を消費しません。これらの型にはsizeは必要ありません。
// Create table with explicit string column types
await tablesDB.createTable({
databaseId: '[DATABASE_ID]',
tableId: ID.unique(),
name: 'articles',
columns: [
{ key: 'title', type: 'varchar', size: 255, required: true }, // inline, fully indexable
{ key: 'summary', type: 'text', required: false }, // off-page, prefix index only
{ key: 'body', type: 'mediumtext', required: false }, // up to ~4 M chars
{ key: 'raw_data', type: 'longtext', required: false }, // up to ~1 B chars
]
});
TypeScript ジェネリクス
import { Models } from 'appwrite';
// Server-side: import from 'node-appwrite'
// Define a typed interface for your row data
interface Todo {
title: string;
done: boolean;
priority: number;
}
// listRows returns Models.DocumentList<Models.Document> by default
// Cast or use generics for typed results
const results = await tablesDB.listRows({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries: [Query.equal('done', false)]
});
// Each document includes built-in fields alongside your data
const doc = results.documents[0];
doc.$id; // string — unique row ID
doc.$createdAt; // string — ISO 8601 creation timestamp
doc.$updatedAt; // string — ISO 8601 update timestamp
doc.$permissions; // string[] — permission strings
doc.$databaseId; // string
doc.$collectionId; // string
// Common model types
// Models.User<Preferences> — user account
// Models.Session — auth session
// Models.File — storage file metadata
// Models.Team — team object
// Models.Execution — function execution result
// Models.DocumentList<T> — paginated list with total count
クエリメソッド
// Filtering
Query.equal('field', 'value') // field == value (or pass array for IN)
Query.notEqual('field', 'value') // field != value
Query.lessThan('field', 100) // field < value
Query.lessThanEqual('field', 100) // field <= value
Query.greaterThan('field', 100) // field > value
Query.greaterThanEqual('field', 100) // field >= value
Query.between('field', 1, 100) // 1 <= field <= 100
Query.isNull('field') // field is null
Query.isNotNull('field') // field is not null
Query.startsWith('field', 'prefix') // string starts with prefix
Query.endsWith('field', 'suffix') // string ends with suffix
Query.contains('field', 'substring') // string/array contains value
Query.search('field', 'keywords') // full-text search (requires full-text index)
// Sorting
Query.orderAsc('field') // sort ascending
Query.orderDesc('field') // sort descending
// Pagination
Query.limit(25) // max rows returned (default 25, max 100)
Query.offset(0) // skip N rows
Query.cursorAfter('[ROW_ID]') // paginate after this row ID (preferred for large datasets)
Query.cursorBefore('[ROW_ID]') // paginate before this row ID
// Selection
Query.select(['field1', 'field2']) // return only specified fields
// Logical
Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR condition
Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // explicit AND (queries are AND by default)
Query.relationship('comments', [Query.equal('approved', true), Query.limit(25)]) // related documents
// Aggregations (optional alias)
Query.count('*', 'total')
Query.sum('amount', 'revenue')
Query.avg('score', 'avgScore')
Query.min('price', 'minPrice')
Query.max('price', 'maxPrice')
// Grouping
Query.groupBy(['status', 'year'])
Query.having([Query.greaterThan('total', 1)])
Query.distinct()
// Joins (table id, left key, right key, operator, alias)
Query.join('orders', '$id', 'customerId')
Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')
Query.leftJoin('orders', 'ord', [
Query.on('$id', 'customerId'),
Query.equal('ord.status', 'paid'),
])
// Fluent builder. Pass build() into list methods.
const queries = Query.builder()
.equal('status', 'published')
.leftJoin('orders', '$id', 'customerId', '=', 'ord')
.count('*', 'total')
.groupBy(['status'])
.limit(25)
.build();
await tablesDB.listRows({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
queries,
});
ファイルストレージ
const storage = new Storage(client);
// Upload file (client-side — from file input)
const file = await storage.createFile({
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: document.getElementById('file-input').files[0]
});
// Upload file (server-side — from path)
import { InputFile } from 'node-appwrite/file';
const file2 = await storage.createFile({
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: InputFile.fromPath('/path/to/file.png', 'file.png')
});
// List files
const files = await storage.listFiles({ bucketId: '[BUCKET_ID]' });
// Get file preview (image)
const preview = storage.getFilePreview({
bucketId: '[BUCKET_ID]',
fileId: '[FILE_ID]',
width: 300,
height: 300
});
// Download file
const download = await storage.getFileDownload({
bucketId: '[BUCKET_ID]',
fileId: '[FILE_ID]'
});
// Delete file
await storage.deleteFile({ bucketId: '[BUCKET_ID]', fileId: '[FILE_ID]' });
InputFile ファクトリメソッド (サーバーサイド)
import { InputFile } from 'node-appwrite/file';
InputFile.fromPath('/path/to/file.png', 'file.png') // from filesystem path
InputFile.fromBuffer(buffer, 'file.png') // from Buffer
InputFile.fromStream(readableStream, 'file.png', size) // from ReadableStream (size in bytes required)
InputFile.fromPlainText('Hello world', 'hello.txt') // from string content
チーム
const teams = new Teams(client);
// Create team
const team = await teams.create({ teamId: ID.unique(), name: 'Engineering' });
// List teams
const list = await teams.list();
// Create membership (invite a user by email)
const membership = await teams.createMembership({
teamId: '[TEAM_ID]',
roles: ['editor'],
email: '[email protected]',
});
// List memberships
const members = await teams.listMemberships({ teamId: '[TEAM_ID]' });
// Update membership roles
await teams.updateMembership({
teamId: '[TEAM_ID]',
membershipId: '[MEMBERSHIP_ID]',
roles: ['admin'],
});
// Delete team
await teams.delete({ teamId: '[TEAM_ID]' });
ロールベースアクセス: 権限を設定する際は、すべてのチームメンバーには
Role.team('[TEAM_ID]')を、特定のチームロールにはRole.team('[TEAM_ID]', 'editor')を使用してください。
リアルタイムサブスクリプション (クライアントサイド)
import { Realtime, Channel } from 'appwrite';
const realtime = new Realtime(client);
// Subscribe to row changes
const subscription = await realtime.subscribe(
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
(response) => {
console.log(response.events); // e.g. ['tablesdb.*.tables.*.rows.*.create']
console.log(response.payload); // the affected resource
}
);
// Subscribe to a specific row
await realtime.subscribe(
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row('[ROW_ID]'),
(response) => { /* ... */ }
);
// Subscribe to multiple channels
await realtime.subscribe([
Channel.tablesdb('[DATABASE_ID]').table('[TABLE_ID]').row(),
Channel.bucket('[BUCKET_ID]').file(),
], (response) => { /* ... */ });
// Unsubscribe
await subscription.close();
利用可能なチャンネル:
| チャンネル | 説明 |
|---|---|
account | 認証済みユーザーのアカウントへの変更 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows | テーブル内のすべての行 |
tablesdb.[DB_ID].tables.[TABLE_ID].rows.[ROW_ID] | 特定の行 |
buckets.[BUCKET_ID].files | バケット内のすべてのファイル |
buckets.[BUCKET_ID].files.[FILE_ID] | 特定のファイル |
teams | ユーザーが所属するチームへの変更 |
teams.[TEAM_ID] | 特定のチームへの変更 |
memberships | ユーザーのチームメンバーシップへの変更 |
memberships.[MEMBERSHIP_ID] | 特定のメンバーシップ |
functions.[FUNCTION_ID].executions | 関数の実行更新 |
response オブジェクトには以下が含まれます: events(イベント文字列の配列)、payload(影響を受けたリソース)、channels(一致したチャンネル)、timestamp(ISO 8601)。
サーバーレス関数 (サーバーサイド)
const functions = new Functions(client);
// Execute function
const execution = await functions.createExecution({
functionId: '[FUNCTION_ID]',
body: JSON.stringify({ key: 'value' })
});
// List executions
const executions = await functions.listExecutions({ functionId: '[FUNCTION_ID]' });
関数ハンドラーの記述 (Node.js ランタイム)
独自の Appwrite 関数をデプロイする場合、エントリポイントファイルはデフォルトの非同期関数をエクスポートする必要があります:
// src/main.js (or src/main.ts)
export default async ({ req, res, log, error }) => {
// Request properties
// req.body — raw request body (string)
// req.bodyJson — parsed JSON body (object, or undefined if not JSON)
// req.headers — request headers (object)
// req.method — HTTP method (GET, POST, PUT, DELETE, PATCH)
// req.path — URL path (e.g. '/hello')
// req.query — parsed query parameters (object)
// req.queryString — raw query string
log('Processing request: ' + req.method + ' ' + req.path);
if (req.method === 'GET') {
return res.json({ message: 'Hello from Appwrite Function!' });
}
const data = req.bodyJson;
if (!data?.name) {
error('Missing name field');
return res.json({ error: 'Name is required' }, 400);
}
// Response methods
return res.json({ success: true }); // JSON (sets Content-Type automatically)
// return res.text('Hello'); // plain text
// return res.empty(); // 204 No Content
// return res.redirect('https://example.com'); // 302 Redirect
// return res.send('data', 200, { 'X-Custom': '1' }); // custom body, status, headers
};
サーバーサイドレンダリング (SSR) 認証
SSR アプリ(Next.js、SvelteKit、Nuxt、Remix、Astro)は、認証を処理するためにサーバー SDK(node-appwrite)を使用します。2 つのクライアントが必要です:
- 管理者クライアント — API キーを使用し、セッションを作成し、レート制限をバイパスします(再利用可能なシングルトン)
- セッションクライアント — セッション Cookie を使用し、ユーザーの代理として動作します(リクエストごとに作成し、決して共有しない)
import { Client, Account, OAuthProvider } from 'node-appwrite';
// Admin client (reusable)
const adminClient = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setKey(process.env.APPWRITE_API_KEY);
// Session client (create per-request)
const sessionClient = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]');
const session = req.cookies['a_session_[PROJECT_ID]'];
if (session) {
sessionClient.setSession(session);
}
メール / パスワードログイン
app.post('/login', async (req, res) => {
const account = new Account(adminClient);
const session = await account.createEmailPasswordSession({
email: req.body.email,
password: req.body.password,
});
// Cookie name must be a_session_<PROJECT_ID>
res.cookie('a_session_[PROJECT_ID]', session.secret, {
httpOnly: true,
secure: true,
sameSite: 'strict',
expires: new Date(session.expire),
path: '/',
});
res.json({ success: true });
});
認証済みリクエスト
app.get('/user', async (req, res) => {
const session = req.cookies['a_session_[PROJECT_ID]'];
if (!session) return res.status(401).json({ error: 'Unauthorized' });
// Create a fresh session client per request
const sessionClient = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
.setProject('[PROJECT_ID]')
.setSession(session);
const account = new Account(sessionClient);
const user = await account.get();
res.json(user);
});
OAuth2 SSR フロー
// Step 1: Redirect to OAuth provider
app.get('/oauth', async (req, res) => {
const account = new Account(adminClient);
const redirectUrl = await account.createOAuth2Token({
provider: OAuthProvider.Github,
success: 'https://example.com/oauth/success',
failure: 'https://example.com/oauth/failure',
});
res.redirect(redirectUrl);
});
// Step 2: Handle callback — exchange token for session
app.get('/oauth/success', async (req, res) => {
const account = new Account(adminClient);
const session = await account.createSession({
userId: req.query.userId,
secret: req.query.secret,
});
res.cookie('a_session_[PROJECT_ID]', session.secret, {
httpOnly: true, secure: true, sameSite: 'strict',
expires: new Date(session.expire), path: '/',
});
res.json({ success: true });
});
Cookie のセキュリティ: XSS を防ぐために、常に
httpOnly、secure、sameSite: 'strict'を使用してください。Cookie 名はa_session_<PROJECT_ID>である必要があります。
ユーザーエージェントの転送: エンドユーザーのブラウザー情報をデバッグとセキュリティのために記録するには、
sessionClient.setForwardedUserAgent(req.headers['user-agent'])を呼び出してください。
エラーハンドリング
import { AppwriteException } from 'appwrite';
// Server-side: import from 'node-appwrite'
try {
const doc = await tablesDB.getRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: '[ROW_ID]',
});
} catch (err) {
if (err instanceof AppwriteException) {
console.log(err.message); // human-readable error message
console.log(err.code); // HTTP status code (number)
console.log(err.type); // Appwrite error type string (e.g. 'document_not_found')
console.log(err.response); // full response body (object)
}
}
一般的なエラーコード:
| コード | 意味 |
|---|---|
401 | 認証エラー — セッションまたは API キーが欠落しているか無効です |
403 | アクセス拒否 — この操作に対する権限が不十分です |
404 | 見つかりません — リソースが存在しません |
409 | 競合 — ID の重複または一意制約違反です |
429 | レート制限 — リクエストが多すぎます。バックオフ後に再試行してください |
権限とロール (重要)
Appwrite はリソースへのアクセスを制御するために権限文字列を使用します。各権限は、アクション (read、update、delete、create、または create + update + delete を付与する write) とロールターゲットを組み合わせたものです。デフォルトでは、行またはファイルのレベルで権限が明示的に設定されていない限り、またはテーブルまたはバケットの設定から継承されていない限り、どのユーザーもアクセスできません。権限は Permission ヘルパーと Role ヘルパーで構築された文字列の配列です。
import { Permission, Role } from 'appwrite';
// Server-side: import from 'node-appwrite'
権限付きのデータベース行
const doc = await tablesDB.createRow({
databaseId: '[DATABASE_ID]',
tableId: '[TABLE_ID]',
rowId: ID.unique(),
data: { title: 'Hello World' },
permissions: [
Permission.read(Role.user('[USER_ID]')), // specific user can read
Permission.update(Role.user('[USER_ID]')), // specific user can update
Permission.read(Role.team('[TEAM_ID]')), // all team members can read
Permission.read(Role.any()), // anyone (including guests) can read
]
});
権限付きのファイルアップロード
const file = await storage.createFile({
bucketId: '[BUCKET_ID]',
fileId: ID.unique(),
file: document.getElementById('file-input').files[0],
permissions: [
Permission.read(Role.any()),
Permission.update(Role.user('[USER_ID]')),
Permission.delete(Role.user('[USER_ID]')),
]
});
権限を設定するタイミング: リソースごとのアクセス制御が必要な場合は、行またはファイルレベルの権限を設定してください。テーブル内のすべての行が同じルールを共有する場合は、テーブルまたはバケットのレベルで権限を設定し、行の権限は空のままにしてください。
よくある間違い:
- 権限の設定忘れ — リソースがすべてのユーザー (作成者を含む) からアクセスできなくなります
write/update/deleteを伴うRole.any()— 未認証のゲストを含むすべてのユーザーがリソースを変更または削除できるようになります- 機密データに対する
Permission.read(Role.any())— リソースが誰でも読み取り可能になります