@websideproject/nuxt-auto-api

Secure REST APIs for Nuxt from your Drizzle schema: deny-by-default authorization, multi-tenancy, validation, soft delete, M2M and TanStack Query composables
2,472 13
Install
npm install @websideproject/nuxt-auto-api
Latest Version:0.1.5
Vue:3
License:MIT
Last Updated:Oct 18, 2026
Links: GitHub  ·  npm
Maintainer: bgervan

@websideproject/nuxt-auto-api

npm version npm downloads license

Schema-driven REST APIs for Nuxt. Register a Drizzle table, declare who may do what, and get list / get / create / update / delete, bulk, aggregations, many-to-many and permission endpoints — plus TanStack Query composables for the client.

  • Secure by default — every operation is denied until you declare who may perform it
  • Row visibility everywhere — tenant scoping, listFilter, soft delete and objectLevel apply to every route: list, get, update, delete, restore, bulk, aggregate, M2M and ?include=
  • Server-owned columns — tenant, soft-delete and audit columns (and the primary key on update, or on create when the database generates it) can never be written by a request body
  • Filtering, sorting, keyset pagination, nested includes, aggregations — restricted to columns the caller may read
  • Soft delete, cascade and restore, field-level permissions, lifecycle hooks, a plugin system
  • SQLite, D1, Turso, Postgres, MySQL, PlanetScale
  • Nuxt 4 (and Nuxt 5 nightly)

Install

npx nuxt module add @websideproject/nuxt-auto-api
npm install drizzle-orm better-sqlite3   # Drizzle + your database's driver; auto-api uses this same Drizzle

Quick start

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@websideproject/nuxt-auto-api', './modules/blog'],
})
// server/plugins/database.ts — hand auto-api your Drizzle instance and name the engine
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { initializeDatabase } from '@websideproject/nuxt-auto-api/database'
import * as schema from '../../modules/blog/schema'

export default defineNitroPlugin(() => {
  initializeDatabase(drizzle(new Database('sqlite.db'), { schema }), 'better-sqlite3') // or 'postgres', 'mysql', 'd1', 'turso', 'planetscale'
})
// modules/blog/schema.ts — a Drizzle table, as you'd write it anyway
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  published: integer('published', { mode: 'boolean' }).notNull().default(false),
  authorId: text('author_id'),
  createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})
// modules/blog/index.ts — register resources from a module
import { defineNuxtModule, createResolver } from '@nuxt/kit'
import { createModuleImport } from '@websideproject/nuxt-auto-api'

export default defineNuxtModule({
  // Nuxt also loads modules/*/index.ts by itself; the name lets it see the entry in nuxt.config is the same module
  meta: { name: 'blog' },
  setup(_, nuxt) {
    const { resolve } = createResolver(import.meta.url)
    nuxt.hook('autoApi:registerSchema', (registry) => {
      registry.register('posts', {
        schema: createModuleImport(resolve('./schema'), 'posts'),
        authorization: createModuleImport(resolve('./auth'), 'postsAuth'),
      })
    })
  },
})
// modules/blog/auth.ts
import { eq, or } from 'drizzle-orm'
import type { ResourceAuthConfig } from '@websideproject/nuxt-auto-api'

export const postsAuth: ResourceAuthConfig = {
  permissions: {
    read: true, // anyone
    create: ctx => !!ctx.user, // signed-in users
    update: ctx => !!ctx.user,
    delete: 'posts:delete', // callers holding this permission
  },
  // Which rows a caller may see — applied to EVERY route, not only lists.
  listFilter: (posts, ctx) => ctx.user ? or(eq(posts.published, true), eq(posts.authorId, ctx.user.id)) : eq(posts.published, true),
  // Per-row check: anyone may read a visible post; only its author may change or delete it.
  objectLevel: (post, ctx) => ctx.operation === 'list' || ctx.operation === 'get' || post.authorId === ctx.user?.id,
}

That's GET/POST /api/posts, GET/PATCH/DELETE /api/posts/:id, /api/posts/bulk, /api/posts/aggregate, /api/posts/:id/relations/:relation, /api/posts/permissions and /api/permissions.

<script setup lang="ts">
const { data } = useAutoApiList('posts', { sort: '-createdAt', limit: 20 })
const { mutate: createPost } = useAutoApiCreate('posts')
</script>

Security model

Undeclared operation Denied (401 anonymous / 403 signed in). true = public, false = nobody — not even *
* permission Super-admin: passes everything except an explicit false
Row visibility Tenant + listFilter + soft delete on every route; objectLevel per row
Request bodies Tenant, soft-delete and audit columns are dropped (the primary key on update too, and on create when the database generates it); field-level write rules refuse with 403
Queries filter / sort / groupBy / aggregates / include filters may only name readable columns — unknown fields and operators are 400s
?include= Needs read on the related resource and applies its row visibility and field rules
Tenancy Resolved on the server only (event.context.tenantId, ctx.user.organizationId, or an extender); fails closed

In the playground

The repo's playground runs these on a seeded database; the pictures are its CI screenshots.

Roles, rows and fields. A regular user's permissions, from GET /api/permissions:

A permission matrix for a regular user: posts allows create, read, update and delete; articles, users and categories allow read only

Nested includes with field selection and a limit:

The include builder: author fields id and name, limit 2, the query /api/posts?include=author[id,name]&limit=2 and its response

Aggregations grouped by a column:

The aggregation builder: sum of id grouped by published, a bar per group and the JSON response

Scoped API tokens: an editor token may read articles and nothing it holds no scope for:

Scope tests with the editor token: GET /api/articles allowed; GET /api/posts, GET /api/users and DELETE /api/articles/1 denied

Documentation

Full documentation: websideproject.com/docs/nuxt-auto

An admin panel for these resources: @websideproject/nuxt-auto-admin.

Links

License

MIT

Related Packages

@x-app/nuxt-api

Configurable API composable for Nuxt 3 and Nuxt 4

393
nuxt-token-authentication

Token Authentication for Nuxt Server APIs

541 5
nuxt-crud-table

API-driven dynamic CRUD table and forms in Nuxt.js. Automatically reflects backe...

830 0
@authup/client-web-nuxt

Nuxt module that signs your app in with Authup and guards its routes by login st...

11,785 41

Version History

Version Released Vue License
0.1.5 ^3.0 MIT
0.1.4 ^3.0 MIT
0.1.3 ^3.0 MIT
0.1.2 ^3.0 MIT
0.1.1 ^3.0 MIT