Back |Launch·

nuxt-auto: a secure REST API from your Drizzle schema

nuxt-auto-api 0.1.0 is out. Register a Drizzle table and you get a REST API with filters, relations and permissions that deny by default. Less backend boilerplate in Nuxt, and why I built it.
Nuxtnuxt-autoDrizzleOpen Source
nuxt-auto: a secure REST API from your Drizzle schema

Why

Too much boilerplate. When I moved to Nuxt for full-stack work, auth was the easy part, Better Auth handles that. The backend was the problem. Every resource meant a new set of endpoints, each with its own permission checks, each tested by hand. I tried oRPC to get a cleaner backend with less of it. Didn't get me there.

I maintain 10+ layers and modules on the same stack, so every endpoint I don't have to write adds up. What I wanted: register the table, get the API. And an admin, if I need one.

So I built nuxt-auto. @websideproject/nuxt-auto-api and @websideproject/nuxt-auto-admin are on 0.1.0 now, the first stable release.

What it looks like

A normal Drizzle table:

server/database/schema.ts
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  slug: text('slug').notNull().unique(),
  content: text('content'),
  published: integer('published', { mode: 'boolean' }).default(false),
  authorId: text('author_id'),
  createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})

The rules. Anything you don't list is refused:

modules/blog/auth.ts
import type { ResourceAuthConfig } from '@websideproject/nuxt-auto-api'
import { eq } from 'drizzle-orm'

export const postsAuth: ResourceAuthConfig = {
  permissions: {
    read: true, // everyone
    create: 'posts:write', // callers with this permission
    update: 'posts:write',
    delete: 'posts:publish',
  },
  // readers don't see drafts at all (404, not 403)
  listFilter: (posts, ctx) =>
    ctx.permissions.includes('posts:write') ? undefined : eq(posts.published, true),
}

And the registration:

modules/blog/index.ts
import { createResolver, defineNuxtModule } from '@nuxt/kit'
import { createModuleImport } from '@websideproject/nuxt-auto-api'

export default defineNuxtModule({
  meta: { name: 'blog' },
  setup(_options, nuxt) {
    const resolver = createResolver(import.meta.url)

    nuxt.hook('autoApi:registerSchema', (registry) => {
      registry.register('posts', {
        schema: createModuleImport(resolver.resolve('../../server/database/schema'), 'posts'),
        authorization: createModuleImport(resolver.resolve('./auth'), 'postsAuth'),
      })
    })
  },
})

You get these routes:

GET    /api/posts        # filters, sorting, pagination, ?include= relations
GET    /api/posts/:id
POST   /api/posts        # validated from the schema
PATCH  /api/posts/:id
DELETE /api/posts/:id

Denied unless you say otherwise

This was the main design rule. A resource registered without authorization refuses every request. Permissions go per operation, per row and per field. If a caller can't see a row, it's a 404 on every route that could reach it: get, update, delete, bulk, aggregates, ?include=. Not only in the list.

The endpoints I still write by hand go through the same permission check, and load rows with helpers that apply the same row rules. So the app has one permission system instead of one per handler.

On the client

TanStack Query composables, auto-imported, typed with your row type:

app/pages/blog.vue
<script setup lang="ts">
import type { posts } from '~~/server/database/schema'

type Post = typeof posts.$inferSelect // a type-only import: no server code reaches the client

const { data, isLoading } = useAutoApiList<Post>('posts', {
  sort: '-createdAt',
  limit: 10,
})
</script>

<template>
  <p v-if="isLoading">Loading…</p>
  <article v-for="post in data?.data" v-else :key="post.id">{{ post.title }}</article>
</template>

What else is in 0.1.0

  • Multi-tenancy. The tenant always comes from the server, never from a header, and requests without one are refused
  • Scoped API tokens, lifecycle hooks, aggregations, many-to-many, soft delete with restore
  • createEndpoint for custom endpoints: the same permission check as the generated routes, plus helpers that load rows with the same row rules
  • The same test suite running on SQLite, D1, Turso, Postgres, MySQL and PlanetScale
  • An optional admin panel built from the same resources and the same permission rules

Compared to what I used before

I used Django for years, and that's where the "less boilerplate" itch comes from. But nuxt-auto isn't a Django port, and in a few places I think it goes further:

  • Django REST Framework allows every request by default (AllowAny) until you add permission classes. Here nothing is allowed until you declare it.
  • Row and field rules are part of the resource config, not something every view has to add.
  • The API and the admin use the same rules. In Django the admin has its own permission system next to DRF's.
  • Types come from the Drizzle schema: the row type Drizzle infers is what you pass to the composables.

In my own apps I don't write about 60-70% of the endpoints anymore, and the rest use the same permission rules. That's from my projects, yours will be different, but it's the reason this exists.

I'll write about the parts separately over the next two weeks: the permission layers, custom endpoints, the composables, multi-tenancy, D1 and the admin.

Try it

npx nuxt module add @websideproject/nuxt-auto-api

Or run the playground. It has around a dozen API demos (roles, row rules, field rules, tokens, bulk operations) with a demo user per role, and the admin:

git clone https://github.com/websideproject/nuxt-auto
cd nuxt-auto
bun install
bun run dev:prepare

cd apps/playground
mkdir -p .data
bun run db:migrate && bun run db:seed   # creates and fills the demo database
cd ../..

bun run dev

MIT licensed. If you try it, I'd like to hear what you think of the permission API, and what you'd want from the admin.

Stay in the loop

Get the latest articles and updates delivered straight to your inbox.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.