CRUD Methods
Nanoka models expose six CRUD methods: findMany, findAll, findOne, create, update, and delete. All methods are bound to the adapter when the model is registered with app.model().
findMany
Fetches multiple rows with pagination. limit is required — omitting it is a TypeScript type error.
// Type error — limit is required
await User.findMany({ offset: 0 })
// ^^^^^^^^ Property 'limit' is missing
// Correct
const users = await User.findMany({ limit: 20, offset: 0 })
const page2 = await User.findMany({ limit: 20, offset: 20 })
Options:
interface FindManyOptions {
limit: number // required
offset?: number // default: 0; runtime cap: 100000
orderBy?: OrderBy // optional ordering
where?: Where | SQL // optional filter
}
where — two forms are accepted:
// Equality object — each key is AND-combined
const users = await User.findMany({ limit: 10, where: { isActive: true } })
// Drizzle SQL expression
import { eq, gt } from 'drizzle-orm'
const users = await User.findMany({
limit: 10,
where: gt(User.table.createdAt, cutoff),
})
orderBy — three forms are accepted:
// Single field name
await User.findMany({ limit: 10, orderBy: 'name' })
// Field with direction
await User.findMany({ limit: 10, orderBy: { column: 'createdAt', direction: 'desc' } })
// Multiple fields
await User.findMany({ limit: 10, orderBy: [{ column: 'name' }, { column: 'createdAt', direction: 'desc' }] })
offset runtime cap: offset is capped at 100,000 to prevent read amplification and DoS. Requests above that throw HTTPException(400). If you need to paginate deeper, use cursor pagination (e.g., id > lastId) instead of offset/limit.
findAll
Fetches all rows without a LIMIT clause. Use this only in batch processing, data exports, or admin tooling — not in request handlers that could receive unbounded input.
const allUsers = await User.findAll()
const activeUsers = await User.findAll({ where: { isActive: true }, orderBy: 'name' })
In request handlers, always prefer findMany with an explicit limit.
findOne
Fetches a single row by primary key value, where clause, or Drizzle SQL expression. Returns null if no row matches.
// By primary key value
const user = await User.findOne('uuid-value')
// By where clause
const user = await User.findOne({ email: 'alice@example.com' })
// By { in: [...] } operator
const user = await User.findOne({ role: { in: ['admin', 'moderator'] } })
// By Drizzle SQL expression
import { eq } from 'drizzle-orm'
const user = await User.findOne(eq(User.table.email, 'alice@example.com'))
// Typical 404 pattern
import { HTTPException } from 'hono/http-exception'
const user = await User.findOne(c.req.param('id'))
if (!user) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(user))
create
Creates a new row and returns the full inserted record including any generated defaults (UUID, timestamps).
const user = await User.create({
email: 'alice@example.com',
name: 'Alice',
})
// Returns the full row including id (auto-generated UUID) and createdAt
The input type is CreateInput<Fields>:
readOnlyfields are optional (can be passed by server code, excluded from API input schemas).serverOnlyfields are completely excluded from the type — passing them tocreate()is a TypeScript error. Useapp.dbdirectly to writeserverOnlyfields.- Fields with a
defaultor markedoptionalare optional in the input. - All other fields are required.
To write a serverOnly field like passwordHash, use the app.db escape hatch:
const passwordHash = await bcrypt.hash(body.password, 10)
await app.db.insert(User.table).values({ ...body, passwordHash })
update
Updates rows matching the given id, where clause, or Drizzle SQL expression. Returns the updated row, or null if no row matched.
// By primary key value
const updated = await User.update('uuid-value', { name: 'Bob' })
// By where clause
const updated = await User.update({ email: 'alice@example.com' }, { name: 'Bob' })
// By { in: [...] } operator
await User.update({ id: { in: ['id-1', 'id-2'] } }, { role: 'suspended' })
// By Drizzle SQL expression
import { inArray } from 'drizzle-orm'
await Session.update(inArray(Session.table.userId, userIds), { expired: true })
// 404 if not found
if (!updated) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(updated))
delete
Deletes rows matching the given id, where clause, or Drizzle SQL expression. Returns { deleted: number }.
const result = await User.delete('uuid-value')
console.log(result.deleted) // number of deleted rows
// By { in: [...] } operator
await Session.delete({ userId: { in: expiredUserIds } })
// inArray chunk splitting — avoids D1 bind limit (100 per query)
import { inArray } from 'drizzle-orm'
const chunkSize = 50
for (let i = 0; i < ids.length; i += chunkSize) {
await Session.delete(inArray(Session.table.id, ids.slice(i, i + chunkSize)))
}
// 404 if nothing was deleted
if (result.deleted === 0) throw new HTTPException(404, { message: 'Not found' })
return c.body(null, 204)
Eager Loading with with
When a model has relations defined (t.hasMany() / t.belongsTo()), pass a with option to load related rows eagerly in a single call.
// Load user with their posts (hasMany)
const user = await User.findOne(id, { with: { posts: true } })
// { id, name, email, createdAt, posts: [{ id, userId, title, ... }] }
// Load post with its author (belongsTo)
const post = await Post.findOne(id, { with: { author: true } })
// { id, userId, title, createdAt, author: { id, name, email, ... } | null }
// findMany also supports with — limit applies to the parent query only
const users = await User.findMany({ limit: 20, with: { posts: true } })
Depth is limited to 1. Nested with is not supported in v1. For complex joins or filtered relations, use app.db. See Relations for the full API.
Full CRUD route example
import { nanoka, d1Adapter } from '@nanokajs/core'
import { HTTPException } from 'hono/http-exception'
export default {
async fetch(req, env, ctx) {
const app = nanoka(d1Adapter(env.DB))
const User = app.model('users', userFields)
// GET /users — paginated list
app.get('/users', async (c) => {
const users = await User.findMany({ limit: 20, offset: 0, orderBy: 'createdAt' })
return c.json(User.toResponseMany(users))
})
// GET /users/:id — single user
app.get('/users/:id', async (c) => {
const user = await User.findOne(c.req.param('id'))
if (!user) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(user))
})
// POST /users — create
app.post('/users', User.validator('json', 'create'), async (c) => {
const body = c.req.valid('json')
const user = await User.create(body)
return c.json(User.toResponse(user), 201)
})
// PATCH /users/:id — partial update
app.patch('/users/:id', User.validator('json', 'update'), async (c) => {
const body = c.req.valid('json')
const updated = await User.update(c.req.param('id'), body)
if (!updated) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(updated))
})
// DELETE /users/:id — delete
app.delete('/users/:id', async (c) => {
const result = await User.delete(c.req.param('id'))
if (result.deleted === 0) throw new HTTPException(404, { message: 'Not found' })
return c.body(null, 204)
})
return app.fetch(req, env, ctx)
},
}
CRUD Methods
Nanoka モデルは 6 つの CRUD メソッドを公開します: findMany・findAll・findOne・create・update・delete。すべてのメソッドは app.model() でモデルを登録する際にアダプターにバインドされます。
findMany
ページネーション付きで複数の行を取得します。limit は必須です — 省略すると TypeScript の型エラーになります。
// 型エラー — limit が必要
await User.findMany({ offset: 0 })
// ^^^^^^^^ Property 'limit' is missing
// 正しい
const users = await User.findMany({ limit: 20, offset: 0 })
const page2 = await User.findMany({ limit: 20, offset: 20 })
オプション:
interface FindManyOptions {
limit: number // 必須
offset?: number // デフォルト: 0; runtime cap: 100000
orderBy?: OrderBy // 任意の並び替え
where?: Where | SQL // 任意のフィルタ
}
where — 2 つの形式が使えます:
// 等値オブジェクト — 各キーは AND で結合される
const users = await User.findMany({ limit: 10, where: { isActive: true } })
// Drizzle SQL 式
import { eq, gt } from 'drizzle-orm'
const users = await User.findMany({
limit: 10,
where: gt(User.table.createdAt, cutoff),
})
orderBy — 3 つの形式が使えます:
// フィールド名のみ
await User.findMany({ limit: 10, orderBy: 'name' })
// 方向付きフィールド
await User.findMany({ limit: 10, orderBy: { column: 'createdAt', direction: 'desc' } })
// 複数フィールド
await User.findMany({ limit: 10, orderBy: [{ column: 'name' }, { column: 'createdAt', direction: 'desc' }] })
offset のランタイム上限: read amplification と DoS を防ぐため、offset は 100,000 で上限化されています。これを超えると HTTPException(400) が throw されます。さらに深いページネーションが必要な場合は、offset/limit ではなくカーソルページネーション(例: id > lastId)を使用してください。
findAll
LIMIT 句なしで全行を取得します。バッチ処理・データエクスポート・管理ツールのみで使用してください。無制限の入力を受け取る可能性があるリクエストハンドラでは使用しないこと。
const allUsers = await User.findAll()
const activeUsers = await User.findAll({ where: { isActive: true }, orderBy: 'name' })
リクエストハンドラでは、常に明示的な limit を持つ findMany を優先してください。
findOne
主キーの値、where 句、または Drizzle SQL 式で単一の行を取得します。行が見つからない場合は null を返します。
// 主キーの値で
const user = await User.findOne('uuid-value')
// where 句で
const user = await User.findOne({ email: 'alice@example.com' })
// { in: [...] } 演算子で
const user = await User.findOne({ role: { in: ['admin', 'moderator'] } })
// Drizzle SQL 式で
import { eq } from 'drizzle-orm'
const user = await User.findOne(eq(User.table.email, 'alice@example.com'))
// 典型的な 404 パターン
import { HTTPException } from 'hono/http-exception'
const user = await User.findOne(c.req.param('id'))
if (!user) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(user))
create
新しい行を作成し、生成されたデフォルト値(UUID・タイムスタンプ)を含む挿入されたレコード全体を返します。
const user = await User.create({
email: 'alice@example.com',
name: 'Alice',
})
// id(自動生成 UUID)と createdAt を含む全行を返す
入力の型は CreateInput<Fields> です:
readOnlyフィールドはオプション(サーバーコードから渡せる。API 入力スキーマからは除外される)。serverOnlyフィールドは型から完全に除外される —create()に渡すと TypeScript エラーになります。serverOnlyフィールドを書き込む場合はapp.dbを直接使用してください。defaultを持つまたはoptionalとマークされたフィールドはオプション。- その他のフィールドはすべて必須。
passwordHash のような serverOnly フィールドを書き込む場合は app.db escape hatch を使います:
const passwordHash = await bcrypt.hash(body.password, 10)
await app.db.insert(User.table).values({ ...body, passwordHash })
update
指定した id、where 句、または Drizzle SQL 式に一致する行を更新します。更新された行を返し、一致する行がなければ null を返します。
// 主キーの値で
const updated = await User.update('uuid-value', { name: 'Bob' })
// where 句で
const updated = await User.update({ email: 'alice@example.com' }, { name: 'Bob' })
// { in: [...] } 演算子で
await User.update({ id: { in: ['id-1', 'id-2'] } }, { role: 'suspended' })
// Drizzle SQL 式で
import { inArray } from 'drizzle-orm'
await Session.update(inArray(Session.table.userId, userIds), { expired: true })
// 見つからない場合は 404
if (!updated) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(updated))
delete
指定した id、where 句、または Drizzle SQL 式に一致する行を削除します。{ deleted: number } を返します。
const result = await User.delete('uuid-value')
console.log(result.deleted) // 削除された行数
// { in: [...] } 演算子で
await Session.delete({ userId: { in: expiredUserIds } })
// inArray チャンク分割 — D1 バインド上限(1 クエリ 100 件)を回避する
import { inArray } from 'drizzle-orm'
const chunkSize = 50
for (let i = 0; i < ids.length; i += chunkSize) {
await Session.delete(inArray(Session.table.id, ids.slice(i, i + chunkSize)))
}
// 削除されなかった場合は 404
if (result.deleted === 0) throw new HTTPException(404, { message: 'Not found' })
return c.body(null, 204)
with を使った eager loading
モデルに relation(t.hasMany() / t.belongsTo())が定義されている場合、with オプションを渡して関連行を 1 回の呼び出しで eager load できます。
// user と posts を一緒に取得(hasMany)
const user = await User.findOne(id, { with: { posts: true } })
// { id, name, email, createdAt, posts: [{ id, userId, title, ... }] }
// post と author を一緒に取得(belongsTo)
const post = await Post.findOne(id, { with: { author: true } })
// { id, userId, title, createdAt, author: { id, name, email, ... } | null }
// findMany でも with が使える — limit は親クエリにのみ適用される
const users = await User.findMany({ limit: 20, with: { posts: true } })
Depth は 1 のみです。ネストした with は v1 非対応です。複雑な join やフィルタ付き relation には app.db を使います。詳細は Relations を参照してください。
フル CRUD ルート例
import { nanoka, d1Adapter } from '@nanokajs/core'
import { HTTPException } from 'hono/http-exception'
export default {
async fetch(req, env, ctx) {
const app = nanoka(d1Adapter(env.DB))
const User = app.model('users', userFields)
// GET /users — ページネーション付きリスト
app.get('/users', async (c) => {
const users = await User.findMany({ limit: 20, offset: 0, orderBy: 'createdAt' })
return c.json(User.toResponseMany(users))
})
// GET /users/:id — 単一ユーザー
app.get('/users/:id', async (c) => {
const user = await User.findOne(c.req.param('id'))
if (!user) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(user))
})
// POST /users — 作成
app.post('/users', User.validator('json', 'create'), async (c) => {
const body = c.req.valid('json')
const user = await User.create(body)
return c.json(User.toResponse(user), 201)
})
// PATCH /users/:id — 部分的な更新
app.patch('/users/:id', User.validator('json', 'update'), async (c) => {
const body = c.req.valid('json')
const updated = await User.update(c.req.param('id'), body)
if (!updated) throw new HTTPException(404, { message: 'Not found' })
return c.json(User.toResponse(updated))
})
// DELETE /users/:id — 削除
app.delete('/users/:id', async (c) => {
const result = await User.delete(c.req.param('id'))
if (result.deleted === 0) throw new HTTPException(404, { message: 'Not found' })
return c.body(null, 204)
})
return app.fetch(req, env, ctx)
},
}