fauxbase 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +187 -513
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,624 +2,298 @@
2
2
 
3
3
  **Start with fake. Ship with real. Change nothing.**
4
4
 
5
- A frontend data layer that simulates your backend entities, services, business logic, 13 query operators, auth, role-based access — all running in the browser. When your backend is ready, swap one config line.
5
+ Fauxbase is a frontend data layer that simulates your backend during development, then connects to your real API without changing your components.
6
6
 
7
7
  ```
8
- npm install @fauxbase/core
8
+ npm install fauxbase
9
9
  ```
10
10
 
11
11
  ---
12
12
 
13
- ## The Problem
14
-
15
- Every frontend project does this:
16
-
17
- ```
18
- 1. Backend not ready → hardcode mock data in components
19
- 2. Mock data grows → copy-paste, no structure, spaghetti
20
- 3. Need filtering/search → hack together Array.filter()
21
- 4. Need pagination → hack together Array.slice()
22
- 5. Need auth → fake login with useState
23
- 6. Backend arrives → REWRITE all data fetching
24
- 7. Partial backend → half mock, half real, chaos
25
- 8. Migration bugs → deadline missed
26
- ```
27
-
28
- Existing tools don't solve this:
29
-
30
- | Tool | What it does | The gap |
31
- |------|-------------|---------|
32
- | **MSW** | Intercepts HTTP at network level | You mock the *transport*, not the *data layer*. No query engine, no auth simulation. |
33
- | **json-server** | Fake REST from a JSON file | No query operators, no auth, no hooks, separate process. |
34
- | **MirageJS** | In-browser mock server | No typed entities, limited query operators, no auth sim, largely abandoned. |
35
- | **Zustand/Redux** | State management | Just state — no CRUD contract, no query engine, no migration path. |
36
-
37
- **Fauxbase fills the gap**: a structured, type-safe data layer with query capabilities and auth that runs locally during development and transparently switches to your real backend.
38
-
39
- ---
40
-
41
- ## Architecture
42
-
43
- ```mermaid
44
- graph TB
45
- subgraph "Your React App"
46
- C[Components]
47
- end
48
-
49
- subgraph "Fauxbase"
50
- S[Service Layer<br/>hooks, validation, CRUD]
51
- QE[QueryEngine<br/>13 operators, sort, paginate]
52
-
53
- subgraph "Driver Abstraction"
54
- LD[LocalDriver<br/>memory / localStorage / indexedDB]
55
- HD[HttpDriver<br/>fetch + preset]
56
- end
57
- end
58
-
59
- subgraph "Storage"
60
- MEM[(Memory)]
61
- LS[(localStorage)]
62
- API[(REST API)]
63
- end
64
-
65
- C -->|fb.product.list, create, ...| S
66
- S --> LD
67
- S --> HD
68
- LD --> QE
69
- LD --> MEM
70
- LD --> LS
71
- HD -->|preset translates| API
72
-
73
- style C fill:#61dafb,color:#000
74
- style S fill:#f5c842,color:#000
75
- style QE fill:#f5c842,color:#000
76
- style LD fill:#4caf50,color:#fff
77
- style HD fill:#2196f3,color:#fff
78
- ```
79
-
80
- **Key insight**: Components always talk to the Service layer. The Service delegates to a Driver. The Driver is swappable. Your components never know whether they're hitting localStorage or a REST API.
81
-
82
- ---
13
+ ## 15-Second Example
83
14
 
84
- ## Quick Start
15
+ Define your data:
85
16
 
86
- ### 1. Define your entities
87
-
88
- ```typescript
89
- import { Entity, field, relation, computed } from '@fauxbase/core';
90
-
91
- export class Product extends Entity {
92
- @field({ required: true }) name!: string;
93
- @field({ required: true, min: 0 }) price!: number;
94
- @field({ default: 0 }) stock!: number;
95
- @field({ default: true }) isActive!: boolean;
96
-
97
- @relation('category') categoryId!: string;
98
-
99
- @computed(p => p.stock > 0 && p.isActive)
100
- available!: boolean;
17
+ ```ts
18
+ class Product extends Entity {
19
+ @field({ required: true }) name!: string;
20
+ @field({ min: 0 }) price!: number;
21
+ @field({ default: 0 }) stock!: number;
101
22
  }
102
23
  ```
103
24
 
104
- The `Entity` base class gives you these fields automatically:
25
+ Use it:
105
26
 
106
- | Field | Type | Description |
107
- |-------|------|-------------|
108
- | `id` | `string` | Auto-generated UUID |
109
- | `createdAt` | `string` | ISO timestamp, set on create |
110
- | `updatedAt` | `string` | ISO timestamp, updated on every change |
111
- | `createdById` | `string?` | User who created (when auth active) |
112
- | `createdByName` | `string?` | Display name of creator |
113
- | `updatedById` | `string?` | User who last updated |
114
- | `updatedByName` | `string?` | Display name of updater |
115
- | `deletedAt` | `string?` | Soft delete timestamp (null = not deleted) |
116
- | `version` | `number` | Auto-incremented on every update |
27
+ ```ts
28
+ const fb = createClient({
29
+ services: { product: ProductService },
30
+ seeds: [seed(Product, [
31
+ { name: 'Hair Clay', price: 185000, stock: 50 },
32
+ { name: 'Beard Oil', price: 125000, stock: 30 },
33
+ ])],
34
+ });
117
35
 
118
- ### 2. Define services with business logic
36
+ // Filter, sort, paginate all work locally
37
+ const result = await fb.product.list({
38
+ filter: { price__gte: 100000 },
39
+ sort: { field: 'price', direction: 'desc' },
40
+ page: 1, size: 20,
41
+ });
42
+ ```
119
43
 
120
- ```typescript
121
- import { Service, beforeCreate, beforeUpdate } from '@fauxbase/core';
122
- import { Product } from './entities/product';
44
+ That's it.
123
45
 
124
- export class ProductService extends Service<Product> {
125
- entity = Product;
126
- endpoint = '/products';
46
+ - Works locally with fake data
47
+ - Switch to real backend later
48
+ - No component changes
127
49
 
128
- @beforeCreate()
129
- ensureUniqueName(data: Partial<Product>, existing: Product[]) {
130
- if (existing.some(p => p.name === data.name)) {
131
- throw new ConflictError(`Product "${data.name}" already exists`);
132
- }
133
- }
50
+ ---
134
51
 
135
- @beforeUpdate()
136
- preventNegativeStock(_id: string, data: Partial<Product>) {
137
- if (data.stock !== undefined && data.stock < 0) {
138
- throw new ValidationError('Stock cannot be negative');
139
- }
140
- }
52
+ ## How It Works
141
53
 
142
- // Custom methods — available on both drivers
143
- async getByCategory(categoryId: string, page = 1) {
144
- return this.list({
145
- filter: { categoryId, isActive: true },
146
- sort: { field: 'name', direction: 'asc' },
147
- page, size: 20,
148
- });
149
- }
150
- }
151
54
  ```
152
-
153
- Services give you:
154
-
155
- | Method | Description |
156
- |--------|-------------|
157
- | `list(query)` | Filtered, sorted, paginated list |
158
- | `get(id)` | Get by ID |
159
- | `create(data)` | Create with hooks + validation |
160
- | `update(id, data)` | Update with hooks |
161
- | `delete(id)` | Soft delete |
162
- | `count(filter?)` | Count matching records |
163
- | `bulk.create([])` | Batch insert |
164
- | `bulk.update([])` | Batch update |
165
- | `bulk.delete([])` | Batch delete |
166
-
167
- ### 3. Define seed data
168
-
169
- ```typescript
170
- import { seed } from '@fauxbase/core';
171
- import { Product } from './entities/product';
172
-
173
- export const productSeed = seed(Product, [
174
- { name: 'Hair Clay', price: 185000, categoryId: 'seed:category:0', stock: 50 },
175
- { name: 'Beard Oil', price: 125000, categoryId: 'seed:category:1', stock: 30 },
176
- ]);
55
+ Development Production
56
+
57
+ Your App Your App
58
+ ↓ ↓
59
+ Fauxbase Fauxbase
60
+ ↓ ↓
61
+ Local Driver HTTP Driver
62
+ (memory / localStorage) ↓
63
+ Your Backend API
177
64
  ```
178
65
 
179
- ### 4. Create the client
180
-
181
- ```typescript
182
- import { createClient } from '@fauxbase/core';
66
+ Components always talk to Fauxbase. Fauxbase talks to a driver. The driver is swappable. Your components never know whether they're hitting localStorage or a REST API.
183
67
 
184
- export const fb = createClient({
185
- driver: import.meta.env.VITE_API_URL
186
- ? { type: 'http', baseUrl: import.meta.env.VITE_API_URL, preset: 'lightwind' }
187
- : { type: 'local', persist: 'localStorage' },
68
+ ---
188
69
 
189
- services: {
190
- product: ProductService,
191
- category: CategoryService,
192
- order: OrderService,
193
- },
70
+ ## The Problem
194
71
 
195
- seeds: [categorySeed, productSeed],
196
- });
72
+ Every frontend project does this:
197
73
 
198
- // Type-safe:
199
- // fb.product.list(...) → ProductService
200
- // fb.category.get(...) → CategoryService
201
- // fb.product.getByCategory() → custom method, fully typed
202
74
  ```
203
-
204
- ### 5. Use it
205
-
206
- ```typescript
207
- // List with filtering
208
- const result = await fb.product.list({
209
- filter: { price__gte: 100000, name__contains: 'hair' },
210
- sort: { field: 'price', direction: 'desc' },
211
- page: 1,
212
- size: 20,
213
- });
214
-
215
- // Create
216
- const { data } = await fb.product.create({
217
- name: 'New Product',
218
- price: 150000,
219
- categoryId: 'seed:category:0',
220
- });
221
-
222
- // Update
223
- await fb.product.update(data.id, { stock: 100 });
224
-
225
- // Delete (soft)
226
- await fb.product.delete(data.id);
75
+ Step 1: Backend not ready → hardcode mock data
76
+ Step 2: Mock data grows → copy-paste everywhere
77
+ Step 3: Need filtering → hack together Array.filter()
78
+ Step 4: Need pagination → hack together Array.slice()
79
+ Step 5: Need auth → fake login with useState
80
+ Step 6: Backend arrives → rewrite everything
227
81
  ```
228
82
 
229
- ---
230
-
231
- ## Query Engine — 13 Operators
232
-
233
- Every filter operator works identically on the local driver (in-memory) and the HTTP driver (translated to URL params).
234
-
235
- ```typescript
236
- const result = await fb.product.list({
237
- filter: {
238
- price__gte: 100000, // price >= 100000
239
- name__contains: 'pomade', // case-insensitive substring
240
- categoryId__in: ['cat-1', 'cat-2'], // value in list
241
- stock__between: [10, 100], // 10 <= stock <= 100
242
- isActive: true, // exact match (eq implied)
243
- description__isnull: false, // is not null
244
- },
245
- sort: { field: 'price', direction: 'desc' },
246
- page: 1,
247
- size: 20,
248
- });
249
- ```
83
+ Existing tools don't solve this:
250
84
 
251
- ### All operators
252
-
253
- | Operator | Syntax | Description | Example |
254
- |----------|--------|-------------|---------|
255
- | `eq` | `field` or `field__eq` | Exact match | `{ isActive: true }` |
256
- | `ne` | `field__ne` | Not equal | `{ status__ne: 'deleted' }` |
257
- | `gt` | `field__gt` | Greater than | `{ price__gt: 100 }` |
258
- | `gte` | `field__gte` | Greater than or equal | `{ price__gte: 100 }` |
259
- | `lt` | `field__lt` | Less than | `{ stock__lt: 10 }` |
260
- | `lte` | `field__lte` | Less than or equal | `{ stock__lte: 100 }` |
261
- | `like` | `field__like` | Case-insensitive substring | `{ name__like: 'hair' }` |
262
- | `contains` | `field__contains` | Same as `like` | `{ name__contains: 'hair' }` |
263
- | `startswith` | `field__startswith` | Case-insensitive prefix | `{ name__startswith: 'ha' }` |
264
- | `endswith` | `field__endswith` | Case-insensitive suffix | `{ email__endswith: '@gmail.com' }` |
265
- | `between` | `field__between` | Inclusive range | `{ price__between: [100, 500] }` |
266
- | `in` | `field__in` | Value in list | `{ status__in: ['active', 'pending'] }` |
267
- | `isnull` | `field__isnull` | Null/undefined check | `{ deletedAt__isnull: true }` |
268
-
269
- ### How queries flow
270
-
271
- ```mermaid
272
- flowchart LR
273
- subgraph "Input"
274
- Q["{ filter: { price__gte: 100 },<br/>sort: { field: 'name', direction: 'asc' },<br/>page: 1, size: 20 }"]
275
- end
276
-
277
- subgraph "QueryEngine"
278
- F[Filter<br/>13 operators]
279
- S[Sort<br/>asc/desc, null-safe]
280
- P[Paginate<br/>slice + meta]
281
- end
282
-
283
- subgraph "Output"
284
- R["{ items: [...],<br/>meta: { page, size,<br/>totalItems, totalPages } }"]
285
- end
286
-
287
- Q --> F --> S --> P --> R
288
- ```
85
+ | Tool | What it does | The gap |
86
+ |------|-------------|---------|
87
+ | **MSW** | Intercepts HTTP | No query engine, no auth, mocks transport not data |
88
+ | **json-server** | Fake REST from JSON | No query operators, no hooks, separate process |
89
+ | **MirageJS** | In-browser server | Limited operators, no typed entities, largely abandoned |
90
+ | **Zustand/Redux** | State management | No CRUD contract, no query engine, no migration path |
289
91
 
290
- Soft-deleted records (where `deletedAt` is set) are automatically excluded before any filtering.
92
+ **Fauxbase removes the rewrite.**
291
93
 
292
94
  ---
293
95
 
294
- ## Drivers
96
+ ## The Key Idea
295
97
 
296
- ### Local Driver (development)
98
+ Fauxbase runs your backend contract in the browser.
297
99
 
298
- Runs entirely in the browser. No server needed.
100
+ Entities define your data model. Services define business logic. A query engine handles filtering, sorting, and pagination.
299
101
 
300
- ```typescript
301
- driver: { type: 'local', persist: 'memory' } // volatile, fastest
302
- driver: { type: 'local', persist: 'localStorage' } // persists across refresh
303
- ```
102
+ During development, it runs locally. When your backend is ready, it forwards the same calls to your API.
304
103
 
305
- | Store | Persists? | Limit | Best for |
306
- |-------|-----------|-------|----------|
307
- | `memory` | No | RAM | Unit tests, throwaway demos |
308
- | `localStorage` | Yes | ~5MB | Default, small-medium datasets |
104
+ ---
309
105
 
310
- ### HTTP Driver (production)
106
+ ## Core Features
311
107
 
312
- Translates Fauxbase calls to fetch requests. Uses **presets** to speak your backend's language.
108
+ - Entity system with decorators (`@field`, `@relation`, `@computed`)
109
+ - Service layer with lifecycle hooks (`@beforeCreate`, `@afterUpdate`, ...)
110
+ - 13 query operators (`eq`, `gte`, `contains`, `between`, `in`, ...)
111
+ - Seed data with deterministic IDs
112
+ - Local driver (memory / localStorage)
113
+ - HTTP driver for real backends
114
+ - Hybrid mode for gradual migration
115
+ - Backend presets (Spring Boot, NestJS, Laravel, Django, Rails, ...)
116
+ - Zero runtime dependencies (~8KB gzipped)
313
117
 
314
- ```typescript
315
- driver: {
316
- type: 'http',
317
- baseUrl: 'https://api.example.com',
318
- preset: 'lightwind',
319
- }
320
- ```
118
+ ---
321
119
 
322
- ### Hybrid Driver (gradual migration)
120
+ ## Hybrid Mode
323
121
 
324
- When your backend is partially ready migrate one service at a time:
122
+ This is the killer feature. Migrate one service at a time:
325
123
 
326
- ```typescript
124
+ ```ts
327
125
  const fb = createClient({
328
- driver: { type: 'local' }, // default for all
126
+ driver: { type: 'local' },
329
127
 
330
128
  overrides: {
331
- product: { driver: { type: 'http', baseUrl: 'https://api.example.com', preset: 'lightwind' } },
332
- // category, order → still local
129
+ product: { driver: { type: 'http', baseUrl: '/api', preset: 'spring-boot' } },
333
130
  },
334
131
  });
335
132
  ```
336
133
 
337
- ### Driver flow
134
+ Products use the real API. Everything else stays local. Migrate at your own pace.
338
135
 
339
- ```mermaid
340
- flowchart TB
341
- C[Component<br/>fb.product.list]
136
+ ---
342
137
 
343
- subgraph "Local Driver"
344
- LS[(Storage<br/>memory/localStorage)]
345
- QE1[QueryEngine<br/>in-memory processing]
346
- end
138
+ ## AI Prototypes → Production
347
139
 
348
- subgraph "HTTP Driver"
349
- PR[Preset<br/>serialize/parse]
350
- FE[fetch<br/>GET /products?...]
351
- API[(REST API)]
352
- end
140
+ Many AI-generated prototypes hardcode arrays:
353
141
 
354
- C -->|dev| LS --> QE1 --> R1[Normalized Response]
355
- C -->|prod| PR --> FE --> API --> PR --> R2[Normalized Response]
142
+ ```ts
143
+ const products = [
144
+ { name: 'Hair Clay', price: 185000 },
145
+ { name: 'Beard Oil', price: 125000 },
146
+ ];
356
147
  ```
357
148
 
358
- Components always get the same normalized response shape, regardless of driver.
149
+ When the prototype becomes real, engineers must rewrite the data layer.
359
150
 
360
- ---
151
+ Fauxbase lets prototypes start with a real data contract:
361
152
 
362
- ## Backend Presets
363
-
364
- A preset tells the HTTP driver how to talk to your backend — how to serialize queries, parse responses, and handle auth.
365
-
366
- ### Built-in presets
153
+ ```
154
+ Claude / Cursor prototype
155
+
156
+ Fauxbase local driver (works immediately)
157
+
158
+ Real backend later (no rewrite)
159
+ ```
367
160
 
368
- | Preset | Framework | Filter Style | Auth |
369
- |--------|-----------|-------------|------|
370
- | `lightwind` | Lightwind (Quarkus) | `?price__gte=100` | `/auth/login` |
371
- | `spring-boot` | Spring Boot | `?price.gte=100` | `/api/auth/signin` |
372
- | `nestjs` | NestJS | `?filter.price.$gte=100` | `/auth/login` |
373
- | `laravel` | Laravel | `?filter[price_gte]=100` | `/api/login` |
374
- | `django` | Django REST Framework | `?price__gte=100` | `/api/token/` |
375
- | `express` | Express.js | `?price__gte=100` | `/auth/login` |
376
- | `fastapi` | FastAPI | `?price__gte=100` | `/api/auth/token` |
377
- | `rails` | Ruby on Rails | `?q[price_gteq]=100` | `/api/login` |
378
- | `go-gin` | Go (Gin) | `?price__gte=100` | `/api/auth/login` |
161
+ ---
379
162
 
380
- ### Custom presets
163
+ ## Query Engine — 13 Operators
381
164
 
382
- ```typescript
383
- import { definePreset } from '@fauxbase/core';
165
+ Every operator works identically on local and HTTP drivers.
384
166
 
385
- const fb = createClient({
386
- driver: {
387
- type: 'http',
388
- baseUrl: 'https://api.myapp.com',
389
- preset: definePreset({
390
- name: 'my-backend',
391
- response: {
392
- single: (raw) => ({ data: raw.result }),
393
- list: (raw) => ({
394
- items: raw.results,
395
- meta: {
396
- page: raw.page,
397
- size: raw.per_page,
398
- totalItems: raw.total,
399
- totalPages: raw.pages,
400
- },
401
- }),
402
- },
403
- query: {
404
- filterStyle: 'django',
405
- pageParam: 'page',
406
- sizeParam: 'per_page',
407
- },
408
- auth: {
409
- loginUrl: '/api/v1/login',
410
- tokenField: 'access_token',
411
- },
412
- }),
167
+ ```ts
168
+ const result = await fb.product.list({
169
+ filter: {
170
+ price__gte: 100000,
171
+ name__contains: 'pomade',
172
+ categoryId__in: ['cat-1', 'cat-2'],
173
+ stock__between: [10, 100],
174
+ isActive: true,
413
175
  },
176
+ sort: { field: 'price', direction: 'desc' },
177
+ page: 1,
178
+ size: 20,
414
179
  });
415
180
  ```
416
181
 
417
- ### How presets work
418
-
419
- ```mermaid
420
- sequenceDiagram
421
- participant Component
422
- participant Service
423
- participant HttpDriver
424
- participant Preset
425
- participant Backend
426
-
427
- Component->>Service: fb.product.list({ filter: { price__gte: 100 } })
428
- Service->>HttpDriver: list("product", query)
429
- HttpDriver->>Preset: serialize query
430
- Preset-->>HttpDriver: GET /products?price__gte=100&page=1&size=20
431
- HttpDriver->>Backend: fetch(url, { headers: { Authorization } })
432
- Backend-->>HttpDriver: { code: 200, data: { items, meta } }
433
- HttpDriver->>Preset: parse response
434
- Preset-->>HttpDriver: { items: [...], meta: { page, size, totalItems, totalPages } }
435
- HttpDriver-->>Service: normalized PagedResponse
436
- Service-->>Component: same shape as local driver
437
- ```
182
+ | Operator | Syntax | Example |
183
+ |----------|--------|---------|
184
+ | `eq` | `field` or `field__eq` | `{ isActive: true }` |
185
+ | `ne` | `field__ne` | `{ status__ne: 'deleted' }` |
186
+ | `gt` | `field__gt` | `{ price__gt: 100 }` |
187
+ | `gte` | `field__gte` | `{ price__gte: 100 }` |
188
+ | `lt` | `field__lt` | `{ stock__lt: 10 }` |
189
+ | `lte` | `field__lte` | `{ stock__lte: 100 }` |
190
+ | `like` | `field__like` | `{ name__like: 'hair' }` |
191
+ | `contains` | `field__contains` | `{ name__contains: 'hair' }` |
192
+ | `startswith` | `field__startswith` | `{ name__startswith: 'ha' }` |
193
+ | `endswith` | `field__endswith` | `{ email__endswith: '@gmail.com' }` |
194
+ | `between` | `field__between` | `{ price__between: [100, 500] }` |
195
+ | `in` | `field__in` | `{ status__in: ['active', 'pending'] }` |
196
+ | `isnull` | `field__isnull` | `{ deletedAt__isnull: true }` |
438
197
 
439
198
  ---
440
199
 
441
- ## Seeding Strategy
442
-
443
- Seed data and runtime data are tracked separately.
444
-
445
- ```mermaid
446
- stateDiagram-v2
447
- [*] --> FirstLoad : App starts
448
- FirstLoad --> HasData : Seeds applied
200
+ ## Services & Hooks
449
201
 
450
- HasData --> HasData : Reloads (seeds unchanged)
451
- note right of HasData : Seeds SKIPPED, data preserved
452
-
453
- HasData --> SeedChanged : Reloads (seeds modified)
454
- SeedChanged --> HasData : Upsert seed records
202
+ ```ts
203
+ class ProductService extends Service<Product> {
204
+ entity = Product;
205
+ endpoint = '/products';
455
206
 
456
- HasData --> ResetSeeds : Reset Seeds
457
- ResetSeeds --> HasData : Wipe + re-seed
207
+ @beforeCreate()
208
+ ensureUniqueName(data: Partial<Product>, existing: Product[]) {
209
+ if (existing.some(p => p.name === data.name)) {
210
+ throw new ConflictError(`Product "${data.name}" already exists`);
211
+ }
212
+ }
458
213
 
459
- HasData --> RefreshSeeds : Refresh Seeds
460
- RefreshSeeds --> HasData : Re-seed only
214
+ async getByCategory(categoryId: string) {
215
+ return this.list({
216
+ filter: { categoryId, isActive: true },
217
+ sort: { field: 'name', direction: 'asc' },
218
+ });
219
+ }
220
+ }
461
221
  ```
462
222
 
463
- **How it works:**
464
-
465
- - Seed records get deterministic IDs: `seed:product:0`, `seed:product:1`, ...
466
- - Runtime records (created during development) get normal UUIDs: `a3f1b2c4-...`
467
- - Fauxbase tracks a `_seedVersion` hash — if your seed definitions change, only seed records are re-applied
468
- - Runtime records are never touched during re-seeding
469
- - On HTTP driver, seeding is disabled entirely — the backend owns the data
223
+ Every service gets: `list`, `get`, `create`, `update`, `delete`, `count`, `bulk.create`, `bulk.update`, `bulk.delete`.
470
224
 
471
225
  ---
472
226
 
473
- ## Response Format
227
+ ## Seeding
474
228
 
475
- All operations return a normalized format. Components always see the same shape regardless of driver.
229
+ Seed data has deterministic IDs. Runtime data has UUIDs. They never collide.
476
230
 
477
- ```typescript
478
- // Single item (get, create, update)
479
- interface ApiResponse<T> {
480
- data: T;
481
- }
482
-
483
- // List (list)
484
- interface PagedResponse<T> {
485
- items: T[];
486
- meta: {
487
- page: number;
488
- size: number;
489
- totalItems: number;
490
- totalPages: number;
491
- };
492
- }
493
-
494
- // Errors (thrown as exceptions)
495
- class NotFoundError // code: 'NOT_FOUND'
496
- class ConflictError // code: 'CONFLICT'
497
- class ValidationError // code: 'VALIDATION', details: { field: message }
498
- class ForbiddenError // code: 'FORBIDDEN'
231
+ ```ts
232
+ const productSeed = seed(Product, [
233
+ { name: 'Hair Clay', price: 185000, stock: 50 }, // → seed:product:0
234
+ { name: 'Beard Oil', price: 125000, stock: 30 }, // → seed:product:1
235
+ ]);
499
236
  ```
500
237
 
501
- ---
502
-
503
- ## Decorators
238
+ - Seeds auto-apply on first load
239
+ - Fauxbase tracks a version hash — if seeds change, only seed records are re-applied
240
+ - Runtime records are never touched during re-seeding
241
+ - On HTTP driver, seeding is disabled — the backend owns the data
504
242
 
505
- ### Entity decorators
243
+ ---
506
244
 
507
- | Decorator | Purpose | Example |
508
- |-----------|---------|---------|
509
- | `@field(options?)` | Mark entity field with validation | `@field({ required: true, min: 0 })` |
510
- | `@relation(entity)` | Foreign key relation | `@relation('category')` |
511
- | `@computed(fn)` | Derived value, recalculated on access | `@computed(p => p.stock > 0)` |
245
+ ## Backend Presets
512
246
 
513
- ### Service hook decorators
247
+ Works with any REST backend. Presets tell the HTTP driver how to serialize queries and parse responses.
514
248
 
515
- | Decorator | Signature | Purpose |
516
- |-----------|-----------|---------|
517
- | `@beforeCreate()` | `(data, existingItems) => void` | Validate/mutate before create |
518
- | `@beforeUpdate()` | `(id, data) => void` | Validate/mutate before update |
519
- | `@afterCreate()` | `(entity) => void` | Side effects after create |
520
- | `@afterUpdate()` | `(entity) => void` | Side effects after update |
249
+ | Preset | Framework | Filter Style |
250
+ |--------|-----------|-------------|
251
+ | `lightwind` | Lightwind (Quarkus) | `?price__gte=100` |
252
+ | `spring-boot` | Spring Boot | `?price.gte=100` |
253
+ | `nestjs` | NestJS | `?filter.price.$gte=100` |
254
+ | `laravel` | Laravel | `?filter[price_gte]=100` |
255
+ | `django` | Django REST Framework | `?price__gte=100` |
256
+ | `express` | Express.js | `?price__gte=100` |
257
+ | `fastapi` | FastAPI | `?price__gte=100` |
258
+ | `rails` | Ruby on Rails | `?q[price_gteq]=100` |
259
+ | `go-gin` | Go (Gin) | `?price__gte=100` |
521
260
 
522
- Hooks can throw errors to abort operations:
523
-
524
- ```typescript
525
- @beforeCreate()
526
- ensureUnique(data: Partial<Product>, existing: Product[]) {
527
- if (existing.some(p => p.name === data.name)) {
528
- throw new ConflictError('Name must be unique');
529
- }
530
- }
531
- ```
261
+ Custom presets supported via `definePreset()`.
532
262
 
533
263
  ---
534
264
 
535
- ## The Migration Timeline
536
-
537
- ```mermaid
538
- gantt
539
- title Frontend Development with Fauxbase
540
- dateFormat YYYY-MM-DD
541
- axisFormat Week %W
542
-
543
- section Setup
544
- Install Fauxbase + define entities/services :a1, 2024-01-01, 1d
545
- Define seed data :a2, after a1, 1d
546
-
547
- section Build (no backend)
548
- Build full UI with local driver :b1, after a2, 28d
549
- CRUD, filtering, pagination, auth — all work :b2, after a2, 28d
550
-
551
- section Migrate (backend arrives)
552
- Products API ready → hybrid mode :c1, after b1, 7d
553
- All APIs ready → full HTTP driver :c2, after c1, 7d
554
- Remove local driver config :c3, after c2, 1d
555
- ```
265
+ ## Migration Timeline
556
266
 
557
267
  ```
558
- Week 1: npm install @fauxbase/core
559
- Define entities, services, seeds
560
- Build UI with local driver — everything works
268
+ Week 1 Install Fauxbase, define entities/services/seeds.
269
+ Build UI with local driver. Everything works.
561
270
 
562
- Week 2-4: Building features at full speed
563
- No blocking on backend, no mock data spaghetti
271
+ Week 2-4 Build features at full speed.
272
+ No blocking on backend. No mock data spaghetti.
564
273
 
565
- Week 5: "Products API is ready"
566
- switch products to HTTP driver (hybrid mode)
567
- zero component changes
274
+ Week 5 "Products API is ready"
275
+ Switch products to HTTP driver (hybrid mode)
276
+ Zero component changes
568
277
 
569
- Week 6: "All APIs ready"
570
- set VITE_API_URL globally
571
- → done
278
+ Week 6 "All APIs ready"
279
+ Set VITE_API_URL → done
572
280
  ```
573
281
 
574
282
  ---
575
283
 
576
- ## Project Structure
577
-
578
- Recommended structure in your app:
579
-
580
- ```
581
- src/
582
- ├── fauxbase/
583
- │ ├── entities/
584
- │ │ ├── product.ts ← class Product extends Entity
585
- │ │ ├── category.ts
586
- │ │ └── user.ts
587
- │ ├── services/
588
- │ │ ├── product.ts ← class ProductService extends Service<Product>
589
- │ │ ├── category.ts
590
- │ │ └── user.ts
591
- │ ├── seeds/
592
- │ │ ├── product.ts ← seed(Product, [...])
593
- │ │ └── category.ts
594
- │ └── index.ts ← createClient({ ... })
595
- ├── components/
596
- │ └── ProductList.tsx ← fb.product.list({ ... })
597
- └── main.tsx
598
- ```
599
-
600
- ---
284
+ ## Who This Is For
601
285
 
602
- ## Technical Details
603
-
604
- | Aspect | Detail |
605
- |--------|--------|
606
- | **Package** | `@fauxbase/core` (~8KB gzipped) |
607
- | **Runtime deps** | Zero |
608
- | **TypeScript** | First-class, full type inference |
609
- | **Decorators** | `experimentalDecorators` (legacy TS decorators) |
610
- | **Query operators** | 13: eq, ne, gt, gte, lt, lte, like, contains, startswith, endswith, between, in, isnull |
611
- | **Storage** | memory, localStorage (indexedDB planned) |
612
- | **Auth** | Planned (Phase 2) |
613
- | **Presets** | lightwind, spring-boot, nestjs, laravel, express, django, fastapi, rails, go-gin, custom |
614
- | **Build** | tsup (ESM + CJS + DTS) |
615
- | **Tests** | vitest, 140+ tests, 97%+ coverage |
286
+ - Frontend teams waiting for backend APIs
287
+ - Solo devs building full-stack apps
288
+ - Prototypers using AI coding tools
289
+ - Teams building UI before backend is ready
616
290
 
617
291
  ---
618
292
 
619
293
  ## Roadmap
620
294
 
621
295
  - [x] **v0.1** — Core: Entity, Service, QueryEngine, LocalDriver, Seeds
622
- - [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`, `useAuth`) + Auth simulation
296
+ - [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`) + Auth simulation
623
297
  - [ ] **v0.3** — HTTP Driver + Backend Presets + DevTools
624
298
  - [ ] **v0.4** — IndexedDB, CLI (`npx fauxbase init`), Vue/Svelte adapters
625
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauxbase",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",