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
  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' }] })

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 or where clause. 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' })

// 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 or where clause. 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' })

// 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 or where clause. Returns { deleted: number }.

const result = await User.delete('uuid-value')
console.log(result.deleted) // number of deleted rows

// 404 if nothing was deleted
if (result.deleted === 0) throw new HTTPException(404, { message: 'Not found' })
return c.body(null, 204)

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)
  },
}