fauxbase 0.1.0 → 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.
- package/README.md +304 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Fauxbase
|
|
2
|
+
|
|
3
|
+
**Start with fake. Ship with real. Change nothing.**
|
|
4
|
+
|
|
5
|
+
Fauxbase is a frontend data layer that simulates your backend during development, then connects to your real API without changing your components.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install fauxbase
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 15-Second Example
|
|
14
|
+
|
|
15
|
+
Define your data:
|
|
16
|
+
|
|
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;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use it:
|
|
26
|
+
|
|
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
|
+
});
|
|
35
|
+
|
|
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
|
+
```
|
|
43
|
+
|
|
44
|
+
That's it.
|
|
45
|
+
|
|
46
|
+
- Works locally with fake data
|
|
47
|
+
- Switch to real backend later
|
|
48
|
+
- No component changes
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
```
|
|
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
|
|
64
|
+
```
|
|
65
|
+
|
|
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.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## The Problem
|
|
71
|
+
|
|
72
|
+
Every frontend project does this:
|
|
73
|
+
|
|
74
|
+
```
|
|
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
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Existing tools don't solve this:
|
|
84
|
+
|
|
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 |
|
|
91
|
+
|
|
92
|
+
**Fauxbase removes the rewrite.**
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## The Key Idea
|
|
97
|
+
|
|
98
|
+
Fauxbase runs your backend contract in the browser.
|
|
99
|
+
|
|
100
|
+
Entities define your data model. Services define business logic. A query engine handles filtering, sorting, and pagination.
|
|
101
|
+
|
|
102
|
+
During development, it runs locally. When your backend is ready, it forwards the same calls to your API.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Core Features
|
|
107
|
+
|
|
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)
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Hybrid Mode
|
|
121
|
+
|
|
122
|
+
This is the killer feature. Migrate one service at a time:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const fb = createClient({
|
|
126
|
+
driver: { type: 'local' },
|
|
127
|
+
|
|
128
|
+
overrides: {
|
|
129
|
+
product: { driver: { type: 'http', baseUrl: '/api', preset: 'spring-boot' } },
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Products use the real API. Everything else stays local. Migrate at your own pace.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## AI Prototypes → Production
|
|
139
|
+
|
|
140
|
+
Many AI-generated prototypes hardcode arrays:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const products = [
|
|
144
|
+
{ name: 'Hair Clay', price: 185000 },
|
|
145
|
+
{ name: 'Beard Oil', price: 125000 },
|
|
146
|
+
];
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When the prototype becomes real, engineers must rewrite the data layer.
|
|
150
|
+
|
|
151
|
+
Fauxbase lets prototypes start with a real data contract:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Claude / Cursor prototype
|
|
155
|
+
↓
|
|
156
|
+
Fauxbase local driver (works immediately)
|
|
157
|
+
↓
|
|
158
|
+
Real backend later (no rewrite)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Query Engine — 13 Operators
|
|
164
|
+
|
|
165
|
+
Every operator works identically on local and HTTP drivers.
|
|
166
|
+
|
|
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,
|
|
175
|
+
},
|
|
176
|
+
sort: { field: 'price', direction: 'desc' },
|
|
177
|
+
page: 1,
|
|
178
|
+
size: 20,
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
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 }` |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Services & Hooks
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
class ProductService extends Service<Product> {
|
|
204
|
+
entity = Product;
|
|
205
|
+
endpoint = '/products';
|
|
206
|
+
|
|
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
|
+
}
|
|
213
|
+
|
|
214
|
+
async getByCategory(categoryId: string) {
|
|
215
|
+
return this.list({
|
|
216
|
+
filter: { categoryId, isActive: true },
|
|
217
|
+
sort: { field: 'name', direction: 'asc' },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Every service gets: `list`, `get`, `create`, `update`, `delete`, `count`, `bulk.create`, `bulk.update`, `bulk.delete`.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Seeding
|
|
228
|
+
|
|
229
|
+
Seed data has deterministic IDs. Runtime data has UUIDs. They never collide.
|
|
230
|
+
|
|
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
|
+
]);
|
|
236
|
+
```
|
|
237
|
+
|
|
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
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Backend Presets
|
|
246
|
+
|
|
247
|
+
Works with any REST backend. Presets tell the HTTP driver how to serialize queries and parse responses.
|
|
248
|
+
|
|
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` |
|
|
260
|
+
|
|
261
|
+
Custom presets supported via `definePreset()`.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Migration Timeline
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Week 1 Install Fauxbase, define entities/services/seeds.
|
|
269
|
+
Build UI with local driver. Everything works.
|
|
270
|
+
|
|
271
|
+
Week 2-4 Build features at full speed.
|
|
272
|
+
No blocking on backend. No mock data spaghetti.
|
|
273
|
+
|
|
274
|
+
Week 5 "Products API is ready"
|
|
275
|
+
→ Switch products to HTTP driver (hybrid mode)
|
|
276
|
+
→ Zero component changes
|
|
277
|
+
|
|
278
|
+
Week 6 "All APIs ready"
|
|
279
|
+
→ Set VITE_API_URL → done
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Who This Is For
|
|
285
|
+
|
|
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
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Roadmap
|
|
294
|
+
|
|
295
|
+
- [x] **v0.1** — Core: Entity, Service, QueryEngine, LocalDriver, Seeds
|
|
296
|
+
- [ ] **v0.2** — React hooks (`useList`, `useGet`, `useMutation`) + Auth simulation
|
|
297
|
+
- [ ] **v0.3** — HTTP Driver + Backend Presets + DevTools
|
|
298
|
+
- [ ] **v0.4** — IndexedDB, CLI (`npx fauxbase init`), Vue/Svelte adapters
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT
|