Basic Usage
Better Query provides built-in CRUD endpoint generation for any resource you define with:
- Type-safe schemas using Zod
- Permission controls per operation (create, read, update, delete, list)
- Automatic client generation with full TypeScript support
But can also be easily extended using plugins, such as: audit, cache, better-auth integration, and more.
Resource Definition
To create CRUD endpoints for a resource, you need to define it using createResource:
import { betterQuery, createResource, withId } from "better-query"
import { z } from "zod"
// Define your schema
const productSchema = withId({
name: z.string().min(1),
price: z.number().min(0),
status: z.enum(["active", "inactive", "draft"]).default("draft"),
})
export const query = betterQuery({
resources: [
createResource({
name: "product",
schema: productSchema,
})
]
})Generated Endpoints
For each resource, the following endpoints are automatically created:
POST /api/product- Create a productGET /api/product/:id- Get a product by IDPATCH /api/product/:id- Update a productDELETE /api/product/:id- Delete a productGET /api/products- List products (with pagination)
Creating Resources
To create a resource, you can use the client method create with the resource data.
import { queryClient } from "@/lib/query-client"; //import the query client
const { data, error } = await queryClient.product.create({
name: "Awesome T-Shirt",
price: 29.99,
status: "active"
}, {
onRequest: (ctx) => {
//show loading
},
onSuccess: (ctx) => {
//redirect to the product page or show success message
},
onError: (ctx) => {
// display the error message
alert(ctx.error.message);
},
});By default, the resources are created with auto-generated IDs and timestamps. You can customize this behavior in your schema definition.
import { betterQuery, createResource } from "better-query"
import { z } from "zod"
const productSchema = z.object({
id: z.string().optional(), // Will be auto-generated if not provided
name: z.string().min(1),
price: z.number().min(0),
createdAt: z.date().default(() => new Date()),
updatedAt: z.date().default(() => new Date()),
})
export const query = betterQuery({
resources: [
createResource({
name: "product",
schema: productSchema,
})
]
})Reading Resources
To read a specific resource, you can use the read function provided by the client.
const { data, error } = await queryClient.product.read("product-id", {
headers: {
"X-API-Key": "your-api-key"
}
});Always invoke client methods from the client side. Don't call them from the server.
Server-Side Resource Management
To manage resources on the server, you can use the query.api methods.
import { query } from "./query"; // path to your Better Query server instance
const response = await query.api.createProduct({
body: {
name: "New Product",
price: 49.99
},
asResponse: true // returns a response object instead of data
});If the server cannot return a response object, you'll need to manually parse and set headers. But for frameworks like Next.js we provide framework-specific helpers to handle this automatically
Updating Resources
To update a resource, you can use the update method provided by the client.
import { queryClient } from "@/lib/query-client"; //import the query client
const { data, error } = await queryClient.product.update("product-id", {
price: 24.99, // Partial updates are supported
status: "active"
}, {
headers: {
"X-API-Key": "your-api-key"
}
});Deleting Resources
To delete a resource, you can use the delete method provided by the client.
import { queryClient } from "@/lib/query-client"; //import the query client
await queryClient.product.delete("product-id", {
headers: {
"X-API-Key": "your-api-key"
}
});You can pass fetchOptions to handle success/error callbacks:
await queryClient.product.delete("product-id", {
fetchOptions: {
onSuccess: () => {
// Redirect to products list or show success message
router.push("/products");
},
onError: (ctx) => {
alert("Failed to delete product");
}
},
});Listing Resources
To list resources with pagination and filtering, you can use the list method.
const { data, error } = await queryClient.product.list({
page: 1,
limit: 10,
search: "shirt", // Search in searchable fields
sortBy: "name",
sortOrder: "asc",
}, {
headers: {
"X-API-Key": "your-api-key"
}
});
console.log(data);
// {
// items: [...products],
// pagination: {
// page: 1,
// limit: 10,
// total: 25,
// totalPages: 3,
// hasNext: true,
// hasPrev: false
// }
// }Resource Management
Once resources are defined, you'll want to control access and manage data. Better Query allows you to easily manage resources from both the server and client sides.
Client Side
Using Client Methods
Better Query provides type-safe client methods to interact with your resources, similar to Better Auth's approach.
import { queryClient } from "@/lib/query-client" // import the query client
export function ProductManager(){
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const fetchProducts = async () => {
setLoading(true);
const result = await queryClient.product.list({
page: 1,
limit: 10
});
if (result.data) {
setProducts(result.data.items);
}
setLoading(false);
};
const createProduct = async (productData) => {
const result = await queryClient.product.create(productData);
if (result.data) {
setProducts(prev => [...prev, result.data]);
}
};
return (
<div>
{loading ? "Loading..." : (
products.map(product => (
<div key={product.id}>{product.name}</div>
))
)}
</div>
)
}import { queryClient } from "~/lib/query-client"; //import the query client
const products = [];
let loading = false;
const fetchProducts = async () => {
loading = true;
const result = await queryClient.product.list({
page: 1,
limit: 10
});
if (result.data) {
products.push(...result.data.items);
}
loading = false;
renderProducts();
};
const renderProducts = () => {
const container = document.getElementById('products');
container.innerHTML = products.map(p => `<div>${p.name}</div>`).join('');
};Server Side
The server provides resource management methods that you can use to handle resources directly. It requires request headers object to be passed when needed.
Example: Using some popular frameworks
import { query } from "./query"; // path to your Better Query server instance
import { headers } from "next/headers";
const product = await query.api.createProduct({
body: {
name: "Server Product",
price: 99.99
},
headers: await headers() // you need to pass the headers object.
})import { query } from "./query";
const app = new Hono();
app.post("/create-product", async (c) => {
const body = await c.req.json();
const product = await query.api.createProduct({
body,
headers: c.req.raw.headers
})
return c.json(product);
});For more details check resource management documentation.
Using Plugins
One of the key features of Better Query is a plugins ecosystem. It allows you to add complex functionality with small lines of code, similar to Better Auth.
Below is an example of how to add audit logging using the audit plugin.
Server Configuration
To add a plugin, you need to import the plugin and pass it to the plugins option of the query instance. For example, to add audit logging, you can use the following code:
import { betterQuery, createResource } from "better-query"
import { auditPlugin } from "better-query/plugins"
export const query = betterQuery({
//...rest of the options
plugins: [
auditPlugin()
]
})now audit-related routes and methods will be available on the server.
Migrate Database
After adding the plugin, you'll need to add the required tables to your database. You can do this by running the migrate command, or by using the generate command to create the schema and handle the migration manually.
generating the schema:
npx better-query generateusing the migrate command:
npx better-query migrateIf you prefer adding the schema manually, you can check the schema required on the audit plugin documentation.
Client Usage
Once the audit plugin is configured on the server, you can access audit functionality through the regular query client API. The audit plugin adds endpoints that can be called from the client:
import { createQueryClient } from "better-query/client";
const queryClient = createQueryClient({
baseURL: "http://localhost:3000/api"
})Now audit-related endpoints are available through the server API:
import { queryClient } from "./query-client"
const getAuditLogs = async() => {
// Call the audit endpoint added by the server plugin
const response = await queryClient.$fetch('/audit/logs', {
query: {
resource: "product", // Filter by resource type
operation: "create", // Filter by operation
page: 1,
limit: 20
}
});
return response;
}Note: Unlike some other frameworks, Better Query doesn't use client-side plugins. All functionality is provided through server endpoints that are accessible via the standard client methods.
Next step: See the audit plugin documentation.