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>:

  • readOnly fields are optional (can be passed by server code, excluded from API input schemas).
  • serverOnly fields are completely excluded from the type — passing them to create() is a TypeScript error. Use app.db directly to write serverOnly fields.
  • Fields with a default or marked optional are 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)
  },
}