Resources
Resources are the core building blocks of Better Query. They define your data models, API endpoints, permissions, and business logic all in one place. Each resource represents a collection of data with automatic CRUD operations and extensive customization options.
Basic Resource Definition
Use createResource() to define a resource with a Zod schema:
import { betterQuery, createResource } from "better-query";
import { z } from "zod";
const userSchema = z.object({
id: z.string().optional(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user"]).default("user"),
createdAt: z.date().optional(),
updatedAt: z.date().optional(),
});
const userResource = createResource({
name: "user",
schema: userSchema,
});
export const query = betterQuery({
basePath: "/api/query",
database: {
provider: "sqlite",
url: "app.db",
autoMigrate: true,
},
resources: [userResource],
});Resource Configuration Options
Schema and Table Name
createResource({
name: "user",
schema: userSchema,
tableName: "users", // Optional: defaults to plural of name
})Endpoint Control
Control which CRUD endpoints are generated:
createResource({
name: "user",
schema: userSchema,
endpoints: {
create: true,
read: true,
update: true,
delete: false, // Disable delete endpoint
list: true,
},
})Custom Endpoints
Add custom endpoints beyond basic CRUD:
createResource({
name: "user",
schema: userSchema,
customEndpoints: {
activate: {
method: "POST",
path: "/users/:id/activate",
handler: async ({ params, context }) => {
const { id } = params;
return await context.adapter.update("user", id, {
status: "active",
activatedAt: new Date()
});
},
},
stats: {
method: "GET",
path: "/users/stats",
handler: async ({ context }) => {
const total = await context.adapter.count("user");
const active = await context.adapter.count("user", {
where: { status: "active" }
});
return { total, active };
},
},
},
})Permissions System
Function-Based Permissions
Define dynamic permissions using functions:
createResource({
name: "post",
schema: postSchema,
permissions: {
create: async (context) => {
// Only authenticated users can create
return !!context.user;
},
read: async () => {
// Anyone can read
return true;
},
update: async (context) => {
// Users can update their own posts, admins can update any
const user = context.user;
if (!user) return false;
if (user.role === "admin") return true;
return context.existingData?.userId === user.id;
},
delete: async (context) => {
// Only admins can delete
return context.user?.role === "admin";
},
list: async (context) => {
// Authenticated users see all, guests see published only
return !!context.user;
},
},
})Permission Context
The permission context provides access to:
user: Current user (null if not authenticated)data: Data being created/updatedexistingData: Existing record (for updates/deletes)params: Route parametersquery: Query parametersrequest: Full request objectresource: Resource nameoperation: Current operation type
permissions: {
update: async (context) => {
const { user, data, existingData, params } = context;
// Check ownership
if (existingData?.userId !== user?.id) {
return false;
}
// Prevent users from changing their own role
if (data.role && data.role !== existingData.role) {
return user?.role === "admin";
}
return true;
},
}Lifecycle Hooks
Hooks allow you to execute code at specific points in the CRUD lifecycle:
Before Hooks
Execute before operations:
createResource({
name: "user",
schema: userSchema,
hooks: {
beforeCreate: async (context) => {
// Hash password before saving
if (context.data.password) {
context.data.password = await hashPassword(context.data.password);
}
// Set timestamps
context.data.createdAt = new Date();
context.data.updatedAt = new Date();
},
beforeUpdate: async (context) => {
// Update timestamp
context.data.updatedAt = new Date();
// Validate email change
if (context.data.email && context.data.email !== context.existingData?.email) {
context.data.emailVerified = false;
// Trigger email verification
await sendVerificationEmail(context.data.email);
}
},
beforeDelete: async (context) => {
// Soft delete instead of hard delete
await context.adapter.update("user", context.params.id, {
deletedAt: new Date(),
status: "deleted",
});
// Prevent the actual delete
throw new Error("SOFT_DELETE_PERFORMED");
},
},
})After Hooks
Execute after successful operations:
hooks: {
afterCreate: async (context) => {
// Send welcome email
await sendWelcomeEmail(context.result.email);
// Create default user settings
await context.adapter.create("userSettings", {
userId: context.result.id,
notifications: true,
theme: "light",
});
},
afterUpdate: async (context) => {
// Log the change
await context.adapter.create("auditLog", {
action: "user.update",
userId: context.user?.id,
targetId: context.result.id,
changes: context.data,
timestamp: new Date(),
});
},
afterDelete: async (context) => {
// Clean up related data
await context.adapter.delete("userSettings", {
where: { userId: context.params.id }
});
},
}Hook Context
Hook contexts provide access to:
data: The data being operated onresult: The operation result (in after hooks)existingData: Current record (for updates/deletes)user: Current authenticated userparams: Route parametersquery: Query parametersrequest: Full request objectadapter: Database adapter for additional queriesresource: Resource nameoperation: Operation type
Relationships
Define relationships between resources:
const userResource = createResource({
name: "user",
schema: userSchema,
relationships: {
posts: {
type: "hasMany",
target: "post",
foreignKey: "userId",
select: ["id", "title", "publishedAt"],
},
profile: {
type: "hasOne",
target: "userProfile",
foreignKey: "userId",
},
roles: {
type: "belongsToMany",
target: "role",
through: "userRoles",
foreignKey: "userId",
targetKey: "roleId",
},
},
});
// Query with relationships
await queryClient.user.list({
include: ["posts", "profile"],
where: { status: "active" },
});Advanced Patterns
Resource-Specific Middleware
Add middleware that runs only for specific resources:
createResource({
name: "sensitiveData",
schema: sensitiveSchema,
middleware: [
{
path: "/sensitive-data/*",
handler: async (context) => {
// Enhanced security for sensitive resources
if (!context.user?.role === "admin") {
throw new Error("Admin access required");
}
// Log all access attempts
await logAccess({
userId: context.user.id,
resource: "sensitiveData",
action: context.operation,
ip: context.request.ip,
userAgent: context.request.headers.get("user-agent"),
});
},
},
],
})Dynamic Schema Validation
Use hooks for complex validation:
hooks: {
beforeCreate: async (context) => {
// Dynamic validation based on user role
if (context.user?.role === "basic") {
const userPostCount = await context.adapter.count("post", {
where: { userId: context.user.id }
});
if (userPostCount >= 5) {
throw new Error("Basic users are limited to 5 posts");
}
}
},
}Computed Fields
Add computed fields using after hooks:
hooks: {
afterRead: async (context) => {
if (context.result) {
// Add computed field
context.result.fullName = `${context.result.firstName} ${context.result.lastName}`;
// Add relationship count
context.result.postCount = await context.adapter.count("post", {
where: { userId: context.result.id }
});
}
},
afterList: async (context) => {
// Add computed fields to list results
for (const item of context.result.data || []) {
item.fullName = `${item.firstName} ${item.lastName}`;
}
},
}Type Safety
Resources provide full TypeScript type safety:
// Infer types from schema
type User = z.infer<typeof userSchema>;
// Generated client is fully typed
const user = await queryClient.user.create({
name: "John Doe",
email: "john@example.com",
// TypeScript will enforce the schema
});
// Return types are inferred
const users: User[] = await queryClient.user.list();Best Practices
1. Resource Organization
// Keep resources in separate files
// resources/user.ts
export const userResource = createResource({ ... });
// resources/post.ts
export const postResource = createResource({ ... });
// lib/query.ts
import { userResource } from "../resources/user";
import { postResource } from "../resources/post";
export const query = betterQuery({
resources: [userResource, postResource],
});2. Permission Helpers
// Create reusable permission functions
const requireAuth = (context: any) => !!context.user;
const requireRole = (role: string) => (context: any) =>
context.user?.role === role;
const requireOwnership = (field = "userId") => (context: any) =>
context.existingData?.[field] === context.user?.id;
// Use in resources
permissions: {
create: requireAuth,
update: requireOwnership("userId"),
delete: requireRole("admin"),
}3. Hook Composition
// Create reusable hooks
const addTimestamps = {
beforeCreate: (context: any) => {
context.data.createdAt = new Date();
context.data.updatedAt = new Date();
},
beforeUpdate: (context: any) => {
context.data.updatedAt = new Date();
},
};
const auditChanges = {
afterCreate: (context: any) => auditLog("create", context),
afterUpdate: (context: any) => auditLog("update", context),
afterDelete: (context: any) => auditLog("delete", context),
};
// Combine hooks
createResource({
name: "user",
schema: userSchema,
hooks: {
...addTimestamps,
...auditChanges,
// Resource-specific hooks
afterCreate: async (context) => {
await sendWelcomeEmail(context.result.email);
},
},
});Resources are the foundation of Better Query applications. They provide a declarative way to define your data models with automatic API generation, while giving you full control over permissions, validation, and business logic through hooks and custom endpoints.