npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-query

v0.1.0

Published

Type-safe CRUD generator with auto-generated REST APIs for Node.js applications

Downloads

10

Readme

Better Query

A standalone, type-safe CRUD generator built on top of better-call that follows the architecture patterns of better-auth. Generate REST API endpoints for your resources with minimal configuration.

Features

  • 🚀 CLI Generator: Get started instantly with npx better-query init --with-auth
  • 🔧 Automatic Endpoint Generation: Creates full CRUD endpoints for any resource
  • Type-Safe: Full TypeScript support with Zod schema validation
  • 🔐 Better Auth Integration: Native integration with Better Auth for authentication and authorization
  • 🔒 Granular Permissions: Configure permissions per operation (create, read, update, delete, list)
  • 🛡️ Role-Based Security: Define permissions based on user roles and organization membership
  • 🔄 Schema Migrations: Automated detection and handling of breaking schema changes
  • 🎛️ Configurable: Enable/disable specific endpoints per resource
  • 📊 Pagination: Built-in pagination support for list endpoints
  • 🔍 Search: Basic search functionality for list operations
  • 🏗️ Database Agnostic: Works with SQLite, PostgreSQL, and MySQL via Kysely
  • 🌐 Framework Agnostic: Works with any framework that supports Web API handlers
  • 🎯 Type-Safe Client: Client SDK with full TypeScript support, similar to better-auth
  • 🎨 Type Generation: Automatic TypeScript type generation from your schemas

Quick Start with CLI

The fastest way to get started is using the Better Query CLI:

# Create a new Next.js project with Better Auth
npx better-query init --with-auth

# Create a basic project
npx better-query init

# Navigate to your project and install dependencies
cd my-project
npm install

# Start development
npm run dev

For complete CLI documentation, see CLI.md.

Installation

npm install better-query
# or
yarn add better-query
# or
pnpm add better-query

Database Dependencies

Install the appropriate database driver for your provider:

# For SQLite
npm install better-sqlite3

# For PostgreSQL  
npm install pg @types/pg

# For MySQL
npm install mysql2

Quick Start

1. Basic Setup

import { betterQuery, createResource, withId } from "better-query";
import { z } from "zod";

// Define your custom schema
const productSchema = withId({
  name: z.string().min(1, "Product name is required"),
  description: z.string().optional(),
  price: z.number().min(0, "Price must be positive"),
  status: z.enum(["active", "inactive", "draft"]).default("draft"),
});

export const query = betterQuery({
  resources: [
    createResource({
      name: "product",
      schema: productSchema,
      permissions: {
        create: () => true,         // Allow all creates
        read: () => true,           // Allow all reads
        update: () => true,         // Allow all updates  
        delete: () => false,        // Disallow all deletes
        list: () => true,           // Allow all lists
      },
    }),
  ],
  database: {
    provider: "sqlite",
    url: "sqlite:./database.db",
    autoMigrate: true,
  },
});

2. Client Setup (NEW!)

Create a type-safe client for your CRUD operations:

import { createQueryClient } from "better-query";

// Create the client with type inference from your query instance
export const queryClient = createQueryClient<typeof query>({
  baseURL: "http://localhost:3000/api",
});

// Now you can use the client with full type safety:
await queryClient.product.create({
  name: "Tee shirt",
  price: 29.99,
}, {
  headers: {
    "Authorization": "Bearer your-token",
  }
});

TypeScript Auto-completion & Type Safety

The client provides full TypeScript support similar to better-auth:

// ✅ Auto-completion for all resources and methods
crudClient.product.create({ /* schema-based suggestions */ });
crudClient.category.list({ /* typed parameters */ });

// ✅ Schema validation - TypeScript enforces required fields
await crudClient.product.create({
  name: "Required field",     // ✅ TypeScript enforces this
  price: 29.99,              // ✅ Validates number type
  status: "active",          // ✅ Enum validation
  // description: "optional"  // ✅ Optional fields suggested
});

// ✅ Partial updates with proper typing
await crudClient.product.update("id", {
  price: 34.99,  // ✅ Only update fields you want to change
});

// ✅ Properly typed responses
const result = await crudClient.product.create(data);
if (result.error) {
  console.log(result.error.code);     // ✅ Typed error codes
} else {
  console.log(result.data.name);      // ✅ Typed response data
}

Error Handling

Better Query includes error codes similar to better-auth:

// Access error codes
console.log(queryClient.$ERROR_CODES.VALIDATION_FAILED);
console.log(queryClient.$ERROR_CODES.FORBIDDEN);
console.log(queryClient.$ERROR_CODES.NOT_FOUND);

// Typed error handling pattern
type ErrorTypes = Partial<
  Record<
    keyof typeof queryClient.$ERROR_CODES,
    {
      en: string;
      es: string;
    }
  >
>;

const errorCodes = {
  VALIDATION_FAILED: {
    en: "validation failed",
    es: "validación fallida",
  },
  FORBIDDEN: {
    en: "access denied",
    es: "acceso denegado", 
  },
} satisfies ErrorTypes;

const getErrorMessage = (code: string, lang: "en" | "es") => {
  if (code in errorCodes) {
    return errorCodes[code as keyof typeof errorCodes][lang];
  }
  return "";
};

// Usage in components
const result = await queryClient.product.create(data);
if (result.error?.code) {
  alert(getErrorMessage(result.error.code, "en"));
}

Better Auth Integration

Better Query integrates with Better Auth through resource permissions and middleware:

import { betterAuth } from "better-auth";
import { betterQuery, createResource } from "better-query";

// 1. Setup Better Auth
const auth = betterAuth({
  database: { provider: "sqlite", url: "auth.db" },
  secret: process.env.BETTER_AUTH_SECRET,
  emailAndPassword: { enabled: true },
});

// 2. Integrate with Better Query using permissions
const query = betterQuery({
  basePath: "/api/query",
  database: { provider: "sqlite", url: "data.db", autoMigrate: true },
  
  resources: [
    createResource({
      name: "product",
      schema: productSchema,
      
      // Add middleware to extract user from Better Auth
      middlewares: [
        {
          handler: async (context) => {
            const session = await auth.api.getSession({
              headers: context.request.headers,
            });
            if (session) context.user = session.user;
          }
        }
      ],
      
      // Use permissions with Better Auth user context
      permissions: {
        create: async (context) => !!context.user, // Authenticated users only
        update: async (context) => {
          const user = context.user as { role?: string; id: string };
          return user?.role === "admin" || 
                 context.existingData?.createdBy === user?.id;
        },
      }
    })
  ]
});

For detailed Better Auth integration guide, see Better Auth Integration Documentation.

3. Framework Integration

Next.js App Router

// app/api/[...query]/route.ts
import { query } from "@/lib/query";

export const GET = query.handler;
export const POST = query.handler;
export const PATCH = query.handler;
export const DELETE = query.handler;

Hono

import { Hono } from "hono";
import { query } from "./query";

const app = new Hono();

app.all("/api/*", async (c) => {
  const response = await query.handler(c.req.raw);
  return response;
});

Express

import express from "express";
import { crud } from "./crud";

const app = express();

app.all("/api/*", async (req, res) => {
  const request = new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, {
    method: req.method,
    headers: req.headers as any,
    body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
  });
  
  const response = await crud.handler(request);
  const data = await response.json();
  
  res.status(response.status).json(data);
});

4. Generated Endpoints

For each resource, the following endpoints are automatically created:

  • POST /api/product - Create a product
  • GET /api/product/:id - Get a product by ID
  • PATCH /api/product/:id - Update a product
  • DELETE /api/product/:id - Delete a product
  • GET /api/products - List products (with pagination)

Client Usage

The client provides a convenient, type-safe way to interact with your CRUD API:

Creating Resources

// Type-safe creation with custom headers
const product = await queryClient.products.create({
  name: "Awesome T-Shirt",
  price: 29.99,
  description: "High quality cotton shirt",
  status: "active",
}, {
  headers: {
    "Authorization": "Bearer your-token",
    "Content-Type": "application/json",
  }
});

Reading Resources

// Get a specific product
const product = await queryClient.products.read("product-id", {
  headers: {
    "Authorization": "Bearer your-token",
  }
});

Updating Resources

// Partial updates are supported
const updatedProduct = await queryClient.products.update("product-id", {
  price: 24.99,
  status: "active",
}, {
  headers: {
    "Authorization": "Bearer your-token",
  }
});

Deleting Resources

await queryClient.products.delete("product-id", {
  headers: {
    "Authorization": "Bearer your-token",
  }
});

Listing Resources with Pagination

const result = await queryClient.products.list({
  page: 1,
  limit: 10,
  search: "shirt",
  sortBy: "name",
  sortOrder: "asc",
}, {
  headers: {
    "Authorization": "Bearer your-token",
  }
});

console.log(result.data); 
// {
//   items: [...products],
//   pagination: {
//     page: 1,
//     limit: 10,
//     total: 25,
//     totalPages: 3,
//     hasNext: true,
//     hasPrev: false
//   }
// }

React Hook Example

import { useState, useEffect } from 'react';
import { queryClient } from '@/lib/query-client';

export function useProducts() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchProducts = async (params = {}) => {
    setLoading(true);
    setError(null);
    try {
      const result = await queryClient.products.list(params, {
        headers: {
          "Authorization": `Bearer ${getAuthToken()}`,
        }
      });
      
      if (result.error) {
        throw new Error(result.error);
      }
      
      setProducts(result.data.items);
      return result.data;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    } finally {
      setLoading(false);
    }
  };

  const createProduct = async (productData) => {
    try {
      const result = await queryClient.products.create(productData, {
        headers: {
          "Authorization": `Bearer ${getAuthToken()}`,
        }
      });
      
      if (result.error) {
        throw new Error(result.error);
      }
      
      const newProduct = result.data;
      setProducts(prev => [...prev, newProduct]);
      return newProduct;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return { products, loading, error, fetchProducts, createProduct };
}

Configuration

Resource Configuration

interface CrudResourceConfig {
  name: string;                    // Resource name (e.g., "product")
  schema: ZodSchema;              // Zod validation schema
  tableName?: string;             // Custom table name (defaults to name)
  endpoints?: {                   // Enable/disable specific endpoints
    create?: boolean;
    read?: boolean;
    update?: boolean;
    delete?: boolean;
    list?: boolean;
  };
  permissions?: {                 // Permission functions
    create?: (context: CrudPermissionContext) => Promise<boolean> | boolean;
    read?: (context: CrudPermissionContext) => Promise<boolean> | boolean;
    update?: (context: CrudPermissionContext) => Promise<boolean> | boolean;
    delete?: (context: CrudPermissionContext) => Promise<boolean> | boolean;
    list?: (context: CrudPermissionContext) => Promise<boolean> | boolean;
  };
}

Client Configuration

interface CrudClientOptions {
  baseURL?: string;               // API base URL
  headers?: Record<string, string>; // Default headers for all requests
  // ... other BetterFetchOption properties
}

CRUD Options

interface CrudOptions {
  resources: CrudResourceConfig[];  // Array of resources
  database: CrudDatabaseConfig;    // Database configuration
  basePath?: string;               // Base path for all endpoints (optional)
  requireAuth?: boolean;           // Global auth requirement (default: false)
  middlewares?: CrudMiddleware[];   // Custom middleware
}

Custom Schemas

Define your own schemas using Zod:

import { z } from "zod";

const userSchema = z.object({
  id: z.string().optional(),
  email: z.string().email(),
  name: z.string().min(1),
  role: z.enum(["user", "admin"]).default("user"),
  createdAt: z.date().default(() => new Date()),
  updatedAt: z.date().default(() => new Date()),
});

const crud = adiemus({
  resources: [
    createResource({
      name: "user",
      schema: userSchema,
    }),
  ],
  database: {
    provider: "sqlite",
    url: "sqlite:./users.db",
  },
});

// Client automatically infers the schema
const client = createCrudClient<typeof crud>();

Schema Helpers

Adiemus provides helpful utilities for creating schemas:

import { withId, withTimestamps } from "adiemus";
import { z } from "zod";

// Helper for creating schemas with id and timestamps
const productSchema = withId({
  name: z.string().min(1),
  price: z.number().positive(),
  description: z.string().optional(),
  status: z.enum(["active", "inactive"]).default("active"),
});

// Helper for creating schemas with just timestamps
const logSchema = withTimestamps({
  level: z.enum(["info", "warn", "error"]),
  message: z.string(),
  source: z.string(),
});

// For maximum flexibility, define schemas manually
const userSchema = z.object({
  id: z.string().optional(),
  email: z.string().email(),
  name: z.string().min(1),
  role: z.enum(["user", "admin"]).default("user"),
  createdAt: z.date().default(() => new Date()),
  updatedAt: z.date().default(() => new Date()),
});

Advanced Usage

Custom Permissions

createResource({
  name: "order",
  schema: orderSchema,
  permissions: {
    create: async (context) => {
      // Only allow users to create orders for themselves
      return context.user && context.data.userId === context.user.id;
    },
    read: async (context) => {
      // Users can only read their own orders, admins can read all
      if (context.user?.role === 'admin') return true;
      const order = await findOrder(context.id);
      return order?.userId === context.user?.id;
    },
    update: async (context) => {
      // Only admin users can update orders
      return context.user?.role === 'admin';
    },
    delete: () => false, // No one can delete orders
    list: async (context) => {
      return !!context.user; // Only authenticated users can list
    },
  },
})

Custom Table Names

createResource({
  name: "product",
  schema: productSchema, // Your custom schema
  tableName: "custom_products_table",
})

Selective Endpoints

createResource({
  name: "user",
  schema: userSchema,
  endpoints: {
    create: true,
    read: true,
    update: true,
    delete: false, // Disable delete endpoint
    list: true,
  },
})

Base Path

const crud = adiemus({
  resources: [/* ... */],
  database: {/* ... */},
  basePath: "/api/v1", // All endpoints will be prefixed with /api/v1
});

Database Support

Adiemus supports multiple database providers through Kysely:

  • SQLite: Perfect for development and small applications
  • PostgreSQL: Production-ready with advanced features
  • MySQL: Wide compatibility and good performance

Auto-Migration

Enable auto-migration to automatically create tables based on your schemas:

const crud = adiemus({
  resources: [/* ... */],
  database: {
    provider: "sqlite",
    url: "sqlite:./database.db",
    autoMigrate: true, // Creates tables automatically
  },
});

Error Handling

Adiemus returns standard HTTP status codes:

  • 200 - Success
  • 201 - Created
  • 400 - Bad Request (validation errors)
  • 401 - Unauthorized
  • 403 - Forbidden (permission denied)
  • 404 - Not Found
  • 500 - Internal Server Error

The client automatically handles these responses:

try {
  const product = await crudClient.products.create({
    name: "Test Product",
    price: 29.99,
  });
  
  if (product.error) {
    // Handle API errors
    console.error('API Error:', product.error);
  } else {
    // Success - use product.data
    console.log('Created:', product.data);
  }
} catch (error) {
  // Handle network/other errors
  console.error('Network Error:', error);
}

Type Safety

The TypeScript integration provides full type safety:

// Types are automatically inferred from your custom schemas
type Product = z.infer<typeof productSchema>;

// CRUD instance is fully typed
const crud = adiemus({...});
// crud.api.createProduct, crud.api.getProduct, etc. are all typed

// Client is fully typed based on your CRUD configuration
const client = createCrudClient<typeof crud>();
// client.products.create, client.products.list, etc. are all typed

Environment Variables

The client can automatically infer the base URL from environment variables:

# Development
BETTER_QUERY_URL=http://localhost:3000/api
# or
NEXT_PUBLIC_BETTER_QUERY_URL=http://localhost:3000/api

# Production
VERCEL_URL=https://your-app.vercel.app/api
# or
NEXT_PUBLIC_VERCEL_URL=https://your-app.vercel.app/api

Contributing

Better Query follows the patterns established by better-auth. When contributing:

  1. Follow the existing code style
  2. Add tests for new functionality
  3. Update documentation
  4. Ensure TypeScript compatibility
  5. Test with multiple database adapters

License

MIT