@flavoai/fastfold
Version:
Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security
937 lines (771 loc) • 25.8 kB
Markdown
# Fastfold
Zero-boilerplate backend for React apps with **Drizzle ORM** integration, auto-generated CRUD, and declarative security. **React Query powered** for smart caching and background sync. Perfect for rapid UI development with minimal code.
## Features
- 🚀 **Zero boilerplate** - Auto-generated CRUD operations from Drizzle schemas
- 🎯 **Simple API** - Just `useQuery` and `useMutation` (powered by React Query!)
- 📦 **Automatic caching** - Smart cache invalidation and background refetching
- 🗄️ **Drizzle ORM** - Full database power with relationships, migrations, and validation
- 🔒 **Declarative security** - Role-based permissions per table
- 🔧 **TypeScript** - Full type safety out of the box
- 🤖 **LLM-friendly** - Small, predictable API surface
- ⚡ **Production ready** - PostgreSQL, MySQL, SQLite support with foreign keys
- 🛠️ **React Query DevTools** - Optional debugging and cache inspection
## Quick Start
### 1. Install Fastfold
```bash
npm install @flavoai/fastfold @tanstack/react-query
```
**Note**: React Query is a peer dependency for the client hooks.
### 2. Define Your Schema with Drizzle (30 seconds)
```typescript
// schema.ts - Use Drizzle's powerful schema definition
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: text('role').default('user'),
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
published: integer('published', { mode: 'boolean' }).default(false),
authorId: integer('author_id').references(() => users.id, { onDelete: 'cascade' }),
});
// Define relationships
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
```
### 3. Create Your Backend (30 seconds)
```typescript
// server.ts - Fastfold adds CRUD + Security to your Drizzle schema
import Fastfold, { Security } from '@flavoai/fastfold';
import path from 'path';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
const db = drizzle(new Database('./app.db'), { schema });
await Fastfold.quickStart({
// 🔗 DRIZZLE INTEGRATION - Use your existing schema
drizzle: { db, schema },
// 🔒 ADD SECURITY - Just specify security per table
tables: {
users: {
security: Security.authenticated(),
operations: ['read', 'update'] // Granular CRUD control
},
posts: {
security: Security.owner('authorId'),
operations: ['create', 'read', 'update', 'delete']
}
},
// 🧩 (Optional) Serve your frontend SPA from the same server
// Supports single or multiple static mounts
staticFrontend: [
{
directory: path.resolve(process.cwd(), 'dist'), // your Vite build output
urlPath: '/', // mount at root
spaFallback: true, // route non-API requests to index.html
excludePaths: ['/api', '/docs', /^\/assets\//],
indexFile: 'index.html',
staticOptions: { index: false, maxAge: '1h', immutable: true }
},
// Example: admin panel mounted at /admin
// {
// directory: path.resolve(process.cwd(), 'admin-dist'),
// urlPath: '/admin',
// spaFallback: true,
// excludePaths: ['/api', '/docs']
// }
]
});
// That's it! Full CRUD API with relationships and security
```
### 4. Build and Serve your Frontend (optional)
If you want to serve your SPA from Fastfold, build it with Vite (or similar) into `dist/` (or your configured directory):
```bash
# From your frontend project
npm run build
# Ensure build outputs to ./dist relative to your server or set staticFrontend.directory accordingly
```
Then start Fastfold; your app will be served along with the API.
### 5. Minimal frontend you can copy-paste (CSP-friendly)
Create `dist/index.html` with:
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fastfold Quickstart</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<h1>Fastfold Quickstart</h1>
<p>Backend API is mounted at <code>/api</code>. Click to seed sample data, then list posts.</p>
<div>
<button id="seed">Seed Sample Data</button>
<button id="load">Load Posts</button>
</div>
<div id="out" style="margin-top: 1rem"></div>
<script defer src="app.js"></script>
</body>
</html>
```
Create `dist/app.js` with:
```js
const out = document.getElementById('out');
const show = (html) => { out.innerHTML = html; };
document.getElementById('seed').onclick = async () => {
const res = await fetch('/api/seed', { method: 'POST', headers: { 'content-type': 'application/json' } });
const data = await res.json();
show(`<div class="card">Seeded: <pre>${JSON.stringify(data, null, 2)}</pre></div>`);
};
document.getElementById('load').onclick = async () => {
const res = await fetch('/api/posts?params=' + encodeURIComponent(JSON.stringify({ with: { author: true }, orderBy: { createdAt: 'desc' } })));
const json = await res.json();
const posts = json.data || [];
show(posts.length ? posts.map(p => `<div class="card"><h3>${p.title}</h3><div>By: ${p.author?.name || 'Unknown'}</div><p>${p.content}</p></div>`).join('') : '<div class="card">No posts yet</div>');
};
```
Create `dist/app.css` with:
```css
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji'; margin: 2rem; }
h1 { margin: 0 0 1rem; }
button { padding: .5rem .75rem; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: .5rem 0; }
code { background: #f6f8fa; padding: .2rem .4rem; border-radius: 4px; }
```
Now start the server and visit `http://localhost:3001`.
### 6. Use in React (30 seconds)
```tsx
// App.tsx - Wrap your app with FastfoldProvider
import { FastfoldProvider, configureFastfold } from '@flavoai/fastfold/client';
// Configure your API endpoint
configureFastfold({ baseUrl: 'http://localhost:3001/api' });
function App() {
return (
<FastfoldProvider>
<BlogList />
</FastfoldProvider>
);
}
// BlogList.tsx - Use Fastfold hooks (powered by React Query!)
import { useQuery, useCreate } from '@flavoai/fastfold/client';
function BlogList() {
// 🎯 SIMPLE QUERY - Fetch data with relationships (with React Query caching!)
const { data: posts, isLoading, error } = useQuery('posts', {
where: { published: true },
orderBy: { createdAt: 'desc' },
with: { author: true } // Include relationships!
});
// 🚀 SIMPLE MUTATION - Create data with automatic cache invalidation
const createPost = useCreate('posts');
const handleCreate = () => {
createPost.mutate({
title: 'Hello World',
content: 'My first post!',
published: true
});
// Cache automatically updates! No manual refetch needed 🎉
};
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button
onClick={handleCreate}
disabled={createPost.isPending}
>
{createPost.isPending ? 'Creating...' : 'Create Post'}
</button>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>By: {post.author?.name}</p>
<p>{post.content}</p>
</div>
))}
</div>
);
}
```
## 🔒 Security Made Simple
Fastfold comes with built-in, declarative security that's both powerful and easy to use:
```typescript
// 🔓 LEVEL 1: ZERO-SECURITY (0 lines of security code)
// Perfect for: public data, prototypes
blog: {
schema: { title: 'string', content: 'string' },
security: Security.public() // Everyone can access ✨
},
// 🔐 LEVEL 2: ONE-LINER SECURITY (1 line of security code)
// Perfect for: most common use cases
adminLogs: {
schema: { action: 'string', timestamp: 'number' },
security: Security.admin() // Only admins ✨
},
userProfiles: {
schema: { userId: 'string', name: 'string', bio: 'string' },
security: Security.owner('userId') // Users own their data ✨
},
// ⚙️ LEVEL 3: CUSTOM SECURITY (as complex as you need)
// Perfect for: complex business rules
projects: {
schema: { name: 'string', teamId: 'string' },
security: Security.custom((ctx) => {
return ctx.user?.teamId === ctx.data?.teamId;
})
}
```
## API Reference
### Server API
```typescript
import Fastfold, { Security } from '@flavoai/fastfold';
// Quick start with Drizzle (recommended)
const adapter = await Fastfold.quickStart({
drizzle: { db, schema },
tables: {
users: {
security: Security.authenticated(),
operations: ['read', 'update']
},
posts: {
security: Security.owner('authorId'),
operations: ['create', 'read', 'update', 'delete']
}
}
});
// Advanced configuration with custom endpoints
const adapter = await Fastfold.quickStart({
drizzle: { db, schema },
tables: { /* your table configs */ },
auth: {
secret: 'your-jwt-secret',
expiresIn: '24h'
},
endpoints: (app) => {
// Add custom endpoints
app.get('/api/custom', (req, res) => {
res.json({ message: 'Custom endpoint' });
});
},
hooks: {
onServerStart: async (server) => {
console.log('Server started on port', server.port);
}
}
});
```
### Client API
```typescript
import {
useQuery, // Fetch multiple records
useQueryOne, // Fetch single record
useCreate, // Create records
useUpdate, // Update records
useDelete, // Delete records
configureFastfold,
setAuthToken
} from '@flavoai/fastfold/client';
// Configure client
configureFastfold({ baseUrl: 'http://localhost:3001/api' });
// Set authentication
setAuthToken('your-jwt-token');
```
#### ⚙️ **Client Configuration**
```tsx
import { configureFastfold, setAuthToken } from '@flavoai/fastfold/client';
// Basic configuration
configureFastfold({
baseUrl: 'https://api.myapp.com/api', // Your API base URL
headers: {
'X-App-Version': '1.0.0',
'X-Custom-Header': 'value'
}
});
// Authentication setup
function LoginComponent() {
const handleLogin = async (email, password) => {
// Your login logic here
const response = await fetch('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
const { token } = await response.json();
// Set auth token for all future requests
setAuthToken(token);
// Now all useQuery, useCreate, etc. will include this token
};
const handleLogout = () => {
// Clear the auth token
setAuthToken('');
// or
configureFastfold({
headers: {} // Clear all headers
});
};
return (
<div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
```
### Complete Client Examples
#### 📋 **Querying Data**
```tsx
import { useQuery, useQueryOne } from '@flavoai/fastfold/client';
function PostsList() {
// Fetch multiple posts with relationships
const { data: posts, isLoading, error } = useQuery('posts', {
where: { published: true },
orderBy: { createdAt: 'desc' },
with: { author: true }, // Include author data
limit: 10
});
// Fetch single post by ID
const { data: post } = useQueryOne('posts', '123', {
with: { author: true, comments: true }
});
if (isLoading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{posts?.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By: {post.author?.name}</p>
<p>{post.content}</p>
</article>
))}
</div>
);
}
```
#### ✏️ **Creating Data**
```tsx
import { useCreate } from '@flavoai/fastfold/client';
function CreatePostForm() {
const createPost = useCreate('posts', {
onSuccess: (newPost) => {
console.log('Post created:', newPost);
// Cache automatically updated - no refetch needed!
},
onError: (error) => {
console.error('Failed to create post:', error);
}
});
const handleSubmit = async (formData) => {
try {
const newPost = await createPost.mutate({
title: formData.title,
content: formData.content,
published: true,
authorId: currentUser.id
});
console.log('Created post:', newPost);
} catch (error) {
console.error('Creation failed:', error);
}
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button
type="submit"
disabled={createPost.isLoading}
>
{createPost.isLoading ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
```
#### 🔄 **Updating Data**
```tsx
import { useUpdate } from '@flavoai/fastfold/client';
function EditPostForm({ post }) {
const updatePost = useUpdate('posts', {
onSuccess: (updatedPost) => {
console.log('Post updated:', updatedPost);
// Navigate back or show success message
},
onError: (error) => {
console.error('Update failed:', error);
}
});
const handleUpdate = async (formData) => {
try {
const updatedPost = await updatePost.mutate({
id: post.id,
data: {
title: formData.title,
content: formData.content,
published: formData.published
}
});
console.log('Updated post:', updatedPost);
} catch (error) {
console.error('Update failed:', error);
}
};
return (
<form onSubmit={handleUpdate}>
{/* form fields */}
<button
type="submit"
disabled={updatePost.isLoading}
>
{updatePost.isLoading ? 'Updating...' : 'Update Post'}
</button>
{updatePost.error && (
<div className="error">
Error: {updatePost.error.message}
</div>
)}
</form>
);
}
```
#### 🗑️ **Deleting Data**
```tsx
import { useDelete } from '@flavoai/fastfold/client';
function PostItem({ post, onDeleted }) {
const deletePost = useDelete('posts', {
onSuccess: () => {
console.log('Post deleted successfully');
onDeleted?.(post.id); // Notify parent component
},
onError: (error) => {
console.error('Delete failed:', error);
}
});
const handleDelete = async () => {
if (confirm('Are you sure you want to delete this post?')) {
try {
await deletePost.mutate(post.id);
} catch (error) {
console.error('Delete failed:', error);
}
}
};
return (
<div className="post-item">
<h3>{post.title}</h3>
<p>{post.content}</p>
<button
onClick={handleDelete}
disabled={deletePost.isLoading}
className="delete-btn"
>
{deletePost.isLoading ? 'Deleting...' : 'Delete'}
</button>
</div>
);
}
```
#### 🔄 **Complete CRUD Example**
```tsx
import { useQuery, useCreate, useUpdate, useDelete } from '@flavoai/fastfold/client';
function PostsManager() {
// Query posts - React Query handles caching automatically
const { data: posts, isLoading } = useQuery('posts', {
with: { author: true },
orderBy: { createdAt: 'desc' }
});
// CRUD operations - cache automatically invalidated!
const createPost = useCreate('posts');
const updatePost = useUpdate('posts');
const deletePost = useDelete('posts');
const handleCreate = (data) => createPost.mutate(data);
const handleUpdate = (id, data) => updatePost.mutate({ id, data });
const handleDelete = (id) => deletePost.mutate(id);
if (isLoading) return <div>Loading...</div>;
return (
<div>
<CreateForm onSubmit={handleCreate} />
{posts?.map(post => (
<PostCard
key={post.id}
post={post}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
);
}
```
#### 🔄 **Cache Management with React Query**
Fastfold uses React Query for automatic cache management! Here's how it works:
##### **Automatic Cache Invalidation (Default Behavior)**
```tsx
function PostsList() {
// React Query handles all caching automatically!
const { data: posts, isLoading, error } = useQuery('posts', {
with: { author: true }
});
const createPost = useCreate('posts');
const updatePost = useUpdate('posts');
const deletePost = useDelete('posts');
const handleCreate = () => {
createPost.mutate({
title: 'New Post',
content: 'Hello World!'
});
// ✨ Cache automatically invalidated and refetched!
// No manual refetch needed!
};
// React Query provides:
// - 5min stale time with background refetch
// - Automatic cache invalidation after mutations
// - Smart deduplication of identical requests
// - Error retry with exponential backoff
}
```
##### **Advanced: Custom Cache Options**
```tsx
function PostsList() {
// Customize React Query behavior
const { data: posts } = useQuery('posts', {
with: { author: true }
}, {
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: false,
refetchInterval: 2 * 60 * 1000 // Poll every 2 minutes
});
const createPost = useCreate('posts', {
onSuccess: (newPost) => {
console.log('✅ Post created:', newPost);
// Cache automatically updates - no manual work needed!
},
onError: (error) => {
console.error('❌ Failed to create post:', error);
// React Query handles retries automatically
}
});
// React Query DevTools (optional)
// Add <ReactQueryDevtools /> to see cache state
}
```
##### **Advanced: Cache Utilities for Power Users**
```tsx
import { useInvalidateCache, useUpdateCache } from '@flavoai/fastfold/client';
function PostEditor() {
const invalidateCache = useInvalidateCache();
const updateCache = useUpdateCache();
const updatePost = useUpdate('posts', {
onSuccess: (updatedPost) => {
// Option 1: Invalidate specific cache keys
invalidateCache(['posts']); // Refetch all posts queries
// Option 2: Update cache directly (optimistic)
updateCache(['posts'], (oldData) =>
oldData?.map(post =>
post.id === updatedPost.id ? updatedPost : post
)
);
}
});
const handleBulkUpdate = async () => {
// For complex operations, invalidate multiple caches
await bulkUpdatePosts();
invalidateCache(['posts', 'users', 'stats']);
};
}
```
> **✨ React Query Power**: Fastfold uses React Query under the hood, giving you all its benefits:
> - **Smart caching** with configurable stale times
> - **Background refetching** to keep data fresh
> - **Automatic deduplication** of identical requests
> - **Error retry** with exponential backoff
> - **Optimistic updates** and rollback on error
> - **DevTools integration** for debugging cache state
>
> Most apps won't need manual cache management - React Query handles it all automatically! 🎉
### Security API
```typescript
import { Security } from '@flavoai/fastfold/client';
Security.public() // Anyone can access
Security.admin() // Only admin role
Security.owner('userId') // Owner-based access
Security.team('teamId') // Team-based access
Security.authenticated() // Any logged-in user
Security.readOnlyPublic() // Public read, admin write
Security.custom((ctx) => boolean) // Custom logic
```
## Auto-Generated API Endpoints
Fastfold automatically creates CRUD endpoints based on your `operations` config:
### Users (operations: ['read', 'update'])
```
GET /api/users ✅ List users
GET /api/users/:id ✅ Get user
PUT /api/users/:id ✅ Update user
POST /api/users ❌ Blocked
DELETE /api/users/:id ❌ Blocked
```
### Posts (operations: ['create', 'read', 'update', 'delete'])
```
GET /api/posts ✅ List posts (with security filtering)
GET /api/posts/:id ✅ Get post
POST /api/posts ✅ Create post
PUT /api/posts/:id ✅ Update post (owner only)
DELETE /api/posts/:id ✅ Delete post (owner only)
// Relationship endpoints (automatic from Drizzle relations)
GET /api/posts/:id/author ✅ Get post's author
GET /api/posts/:id/comments ✅ Get post's comments
GET /api/users/:id/posts ✅ Get user's posts
```
### Query Parameters
```
GET /api/posts?where={"published":true}&orderBy={"createdAt":"desc"}&limit=10
GET /api/posts?with={"author":true,"comments":true} // Include relations
```
## Examples
See the `examples/` directory for complete examples:
- [Quickstart Backend](examples/quickstart.ts)
- [React Client Usage](examples/react-client.tsx)
## Development
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Run tests
npm test
```
## Database Operations
Fastfold automatically handles database setup with zero configuration:
```bash
# Auto-migration: Tables are created automatically when server starts
npm run dev # Creates tables from your schema definitions
# Reset database: Simply delete the database file
rm ./fastfold.db # Default SQLite database file
npm run dev # Recreates tables on next start
# Custom database location
const adapter = await Fastfold.quickStart({
drizzle: {
db: drizzle(new Database('./my-app.db'), { schema }),
schema
},
tables: { /* your table configs */ }
});
```
**Note**: Fastfold uses auto-migration - tables are created/updated automatically based on your schema definitions. No manual migration commands needed.
## Advanced Features
### 🔒 Granular CRUD Control
Control exactly which operations are allowed per table:
```typescript
await Fastfold.quickStart({
drizzle: { db, schema },
tables: {
users: {
security: Security.authenticated(),
operations: ['read', 'update'] // Only GET and PUT endpoints
},
posts: {
security: Security.owner('authorId'),
operations: ['create', 'read', 'update', 'delete'] // Full CRUD
},
comments: {
security: Security.custom((ctx) => ctx.user?.id === ctx.data?.authorId),
operations: ['create', 'read', 'delete'] // No updates allowed
}
}
});
```
### 🎯 Custom Endpoints with Full Drizzle Power
```typescript
const adapter = await Fastfold.quickStart({
drizzle: { db, schema },
tables: { /* your table configs */ },
endpoints: (app) => {
// Use Drizzle's relational queries
app.get('/api/trending', Security.public(), async (req, res) => {
const trending = await adapter.queryWithRelations('posts', {
with: {
author: { columns: { name: true } },
comments: true
},
where: { published: true },
orderBy: { createdAt: 'desc' },
limit: 10
});
res.json(trending);
});
// Complex business logic
app.post('/api/newsletter', Security.admin(), async (req, res) => {
const users = await adapter.query('users', {
where: { role: 'subscriber' }
});
// Send emails...
res.json({ sent: users.length });
});
}
});
// Use adapter anywhere in your app
export { adapter };
```
### 🚀 Advanced Configuration
```typescript
await Fastfold.quickStart({
drizzle: { db, schema },
tables: { /* your table configs */ },
// Authentication configuration
auth: {
secret: 'your-jwt-secret',
expiresIn: '24h'
},
// Custom endpoints
endpoints: (app) => {
app.get('/api/trending', async (req, res) => {
const trending = await adapter.queryWithRelations('posts', {
with: { author: true },
where: { published: true },
orderBy: { createdAt: 'desc' },
limit: 10
});
res.json(trending);
});
},
// Server hooks
hooks: {
onServerStart: async (server) => {
console.log('🚀 Server ready on port', server.port);
}
}
});
});
```
## Why Fastfold?
**Perfect for LLMs and rapid development:**
1. **Drizzle ORM Power** - Full database relationships, migrations, validation
2. **Tiny API surface** - Just define Drizzle schema + security rules
3. **Zero boilerplate** - Auto-generated CRUD with granular control
4. **Type-safe by default** - Full TypeScript from database to frontend
5. **Production ready** - PostgreSQL, MySQL, SQLite with foreign keys
6. **Extensible** - Full Express access + Drizzle queries when needed
**Perfect for:**
✅ **Production applications** - Full database power with Drizzle
✅ **Rapid prototyping** - Zero-config CRUD generation
✅ **LLM-assisted development** - Minimal, predictable API
✅ **React + Backend** - Unified TypeScript experience
✅ **Complex data models** - Relationships, constraints, validation
✅ **Custom business logic** - Express endpoints + Drizzle queries
**Not suitable for:**
❌ **Microservices architecture** - Single server design
❌ **Non-database apps** - Focused on CRUD operations
❌ **GraphQL requirements** - REST API only
❌ **Real-time by default** - WebSockets require custom endpoints
## License
MIT