@koltakov/ffa-core
v0.16.1
Published
Instant mock REST API for frontend development
Maintainers
Readme
ffa-core
Frontend First API — instant mock REST API for frontend development.
Stop waiting for the backend. Describe your data in one config file, run one command, get a fully working REST API.
npx @koltakov/ffa-core init
npx @koltakov/ffa-core devWhy ffa?
- Zero boilerplate — one config file, one command
- Auto CRUD — 6 routes per entity, out of the box
- Smart fake data — field names drive generation (
email,price,avatar, ...) - Typed DSL —
string().email().required()with full TypeScript autocomplete - Zod validation — POST/PUT/PATCH bodies validated against your schema
- Relations —
belongsTo/hasManywith inline join via?include= - Swagger UI — auto-generated docs at
/docs - Delay simulation — test loading states with artificial latency
- TypeScript or JSON — pick your config format
Quick Start
npm install @koltakov/ffa-core
npx ffa init # scaffold ffa.config.ts
npx ffa dev # start serverConfig
// ffa.config.ts
import {
defineConfig, entity,
string, number, boolean, enumField,
belongsTo, hasMany, object, array,
} from '@koltakov/ffa-core'
export default defineConfig({
server: {
port: 3333,
delay: [200, 600], // artificial latency in ms (number or [min, max])
persist: true, // save data to ffa-data.json between restarts
errorRate: 0.05, // 5% of requests return 500 (for error state testing)
},
entities: {
User: entity({
name: string().fullName().required(),
email: string().email().required(),
role: enumField(['admin', 'editor', 'viewer']).required(),
avatar: string().avatar().optional(),
address: object({
city: string().city(),
country: string().country(),
zip: string().zip(),
}),
phones: array(string().phone()),
}, {
count: 20,
// Guaranteed records — appear first, rest are generated up to count
seed: [
{ name: 'Admin User', email: '[email protected]', role: 'admin' },
],
}),
Post: entity({
title: string().sentence().required(),
body: string().paragraph().required(),
status: enumField(['draft', 'published', 'archived']).required(),
price: number().price().required(),
rating: number().rating().optional(),
authorId: belongsTo('User'),
tagIds: hasMany('Tag'),
}, {
count: 50,
meta: {
currency: 'USD', // static value
total: (items) => items.length, // computed from filtered items
avgPrice: (items) => +(
items.reduce((s, i) => s + (i.price as number), 0) / items.length
).toFixed(2),
},
}),
Tag: entity({
name: string().word().required(),
color: string().hexColor().optional(),
}, { count: 15 }),
},
})CLI
| Command | Description |
|---------|-------------|
| ffa dev | Start the dev server |
| ffa dev -p 4000 | Override port |
| ffa dev -w | Watch mode — restart on config changes |
| ffa dev -o | Open Swagger UI in browser on start |
| ffa init | Scaffold ffa.config.ts in current directory |
| ffa inspect | Show config structure without starting the server |
| ffa snapshot | Export current in-memory DB to JSON (server must be running) |
| ffa snapshot -o dump.json | Custom output path |
| ffa reset | Delete ffa-data.json (clears persisted data) |
Generated Endpoints
For every entity Foo ffa creates six routes:
| Method | Path | Description | Status |
|--------|------|-------------|--------|
| GET | /foos | List all records | 200 |
| GET | /foos/:id | Get single record | 200, 404 |
| POST | /foos | Create a record | 201, 422 |
| PUT | /foos/:id | Full replace | 200, 404, 422 |
| PATCH | /foos/:id | Partial update | 200, 404, 422 |
| DELETE | /foos/:id | Delete a record | 200, 404 |
Names are auto-pluralized:
Product→/products,Category→/categories.
System endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | /__reset | Regenerate all data |
| GET | /__snapshot | Dump current DB to JSON |
| GET | /docs | Swagger UI |
| GET | /openapi.json | OpenAPI 3.0 spec |
Query Params (GET /list)
Pagination, sorting, search
?page=2&limit=10
?sort=price&order=desc
?search=apple full-text search across all string fields
?status=published exact match filterFilter operators
?price_gte=100 price >= 100
?price_lte=500 price <= 500
?price_gt=0 price > 0
?price_lt=1000 price < 1000
?price_ne=0 price != 0
?status_in=draft,published status in ['draft', 'published']
?title_contains=hello title contains 'hello' (case-insensitive)All params are combinable:
GET /posts?search=react&status_in=draft,published&price_gte=10&sort=price&order=asc&page=1&limit=20Response envelope
{
"data": [{ "id": "...", "title": "...", "price": 49.99 }],
"meta": {
"pagination": { "total": 47, "page": 1, "limit": 10, "pages": 5 },
"currency": "USD",
"avgPrice": 124.50
}
}Header: X-Total-Count: 47
Field Types
Primitives
string() // chainable hints — see below
number() // chainable hints — see below
boolean()
uuid()
datetime()Enum
enumField(['draft', 'published', 'archived'])Relations
belongsTo('User') // stores a random User id
hasMany('Tag') // stores an array of 1–3 Tag idsInline join on GET /:id:
GET /posts/abc?include=authorId,tagIdsNested structures (v0.12.0)
// Nested object — generates each field recursively
address: object({
city: string().city(),
country: string().country(),
zip: string().zip(),
})
// Array of typed items
tags: array(string().word())
phones: array(string().phone(), [1, 4]) // [min, max] items
scores: array(number().rating())String Fake Hints
Chain a method on string() to control what gets generated:
// Internet
string().email() // "[email protected]"
string().url() // "https://example.com"
string().domain() // "example.com"
string().ip() // "192.168.1.1"
string().username() // "john_doe"
// Media
string().image() // "https://picsum.photos/seed/xxx/1280/720" (default 16:9)
string().image(300, 450) // "https://picsum.photos/seed/xxx/300/450" (portrait 2:3)
string().image(1920, 1080) // "https://picsum.photos/seed/xxx/1920/1080" (Full HD)
string().avatar() // avatar URL
// Person
string().firstName() // "Alice"
string().lastName() // "Johnson"
string().fullName() // "Alice Johnson"
string().phone() // "+1-555-234-5678"
// Location
string().city() // "Berlin"
string().country() // "Germany"
string().address() // "12 Oak Street"
string().zip() // "10115"
string().locale() // "DE"
// Business
string().company() // "Acme Corp"
string().jobTitle() // "Senior Engineer"
string().department() // "Electronics"
string().currency() // "EUR"
// Text
string().word() // "matrix"
string().slug() // "hello-world"
string().sentence() // "The quick brown fox."
string().paragraph() // "Lorem ipsum dolor..."
string().bio() // "Lorem ipsum dolor..."
// Visual
string().color() // "azure"
string().hexColor() // "#A3F5C2"
// Id
string().uuid() // UUID v4Number Fake Hints
number().price() // 19.99
number().age() // 34
number().rating() // 4
number().percent() // 72
number().lat() // 51.5074
number().lng() // -0.1278
number().year() // 2021Smart Field-Name Detection
When no hint is set, ffa infers the right value from the field name automatically:
| Field name pattern | Generated value |
|--------------------|----------------|
| email, mail | Email address |
| name, firstName | First name |
| lastName, surname | Last name |
| phone, tel, mobile | Phone number |
| city, country, address | Location values |
| url, website, link | URL |
| avatar, photo | Avatar URL |
| image | Image URL |
| company | Company name |
| title, heading | Short sentence |
| description, bio, text | Paragraph |
| price, cost, amount | Price |
| color | Color name |
Field Rules
All builders share these chainable rules:
| Method | Effect |
|--------|--------|
| .required() | Field required in POST/PUT |
| .optional() | Field can be omitted (default) |
| .min(n) | Min string length / min number value |
| .max(n) | Max string length / max number value |
| .readonly() | Excluded from create/update validation |
| .default(val) | Default value |
| .fake(hint) | Explicit faker hint |
| .image(w, h) | Image dimensions (only for string().image()) |
Seed Data (v0.9.0)
Guarantee specific records always exist in your entity:
User: entity({
name: string().fullName().required(),
role: enumField(['admin', 'editor', 'viewer']).required(),
}, {
count: 20,
seed: [
{ id: 'admin-1', name: 'Admin User', role: 'admin' },
{ name: 'Guest User', role: 'viewer' }, // id auto-generated if omitted
],
})Seed records appear first. The remaining count - seed.length records are generated.
Custom Meta (v0.8.0)
Attach extra fields to the list response meta. Values can be static or computed from the current filtered dataset (before pagination):
Product: entity({ price: number().price(), status: enumField(['active', 'sale']) }, {
count: 50,
meta: {
currency: 'USD', // static
apiVersion: 2, // static
total: (items) => items.length, // computed
avgPrice: (items) => +(items.reduce((s, i) => s + (i.price as number), 0) / items.length).toFixed(2),
byStatus: (items) => Object.fromEntries(
['active', 'sale'].map(s => [s, items.filter(i => i.status === s).length])
),
},
})Meta functions receive items after search/filter but before pagination — so aggregates always reflect the current query.
Seed Data Export (v0.15.0)
Export the current in-memory DB while the server is running — useful for generating fixture files:
ffa snapshot # → ffa-snapshot.json
ffa snapshot -o fixtures.json
ffa snapshot -p 4000 # specify port if not in configOr call directly:
GET /__snapshot → { "User": [...], "Post": [...] }Inspect Config (v0.16.0)
Print your config structure without starting the server:
ffa inspect FFA inspect port 3333
User 20 records +1 seed
* name string [fullName]
* email string [email]
* role enum (admin | editor | viewer)
avatar string [avatar]
address object { city, country, zip }
phones array of string
Post 50 records
* title string [sentence]
* body string [paragraph]
* status enum (draft | published | archived)
* price number [price]
authorId belongsTo → User
tagIds hasMany → Tag
meta: currency, total, avgPrice
delay 200–600ms · persist ffa-data.json · errorRate 5%Config Validation (v0.11.0)
On startup, ffa checks your config and prints warnings:
⚠ Post.authorId (belongsTo) references 'Author' which is not defined
⚠ Tag.type is enum but has no values
⚠ User: seed has 5 records but count is 3 — seed will be truncatedError Simulation (v0.14.0)
Test your frontend error handling without mocking:
server: {
errorRate: 0.1 // 10% of requests return 500
}System routes (/__reset, /__snapshot, /docs) are never affected.
Persistence
server: {
persist: true, // save to ./ffa-data.json
persist: './data/db.json', // custom path
}Data is saved on every write (POST/PUT/PATCH/DELETE) and restored on restart.
JSON Config (no TypeScript)
{
"server": { "port": 3333, "delay": 300 },
"entities": {
"Product": {
"count": 20,
"fields": {
"title": "string!",
"price": "number",
"status": ["draft", "published", "archived"],
"inStock": "boolean",
"createdAt": "datetime"
}
}
}
}Save as ffa.config.json. "string!" = required, "string" = optional. Arrays are shorthand for enumField.
Terminal Output
FFA dev server v0.16.0
Entity Count Route
──────────────────────────────────────────────────────────
User 20 GET POST http://localhost:3333/users
Post 50 GET POST http://localhost:3333/posts
Tag 15 GET POST http://localhost:3333/tags
delay 200–600ms
errorRate 5% chance of 500
docs http://localhost:3333/docs
reset POST http://localhost:3333/__reset
GET /users 200 14ms
POST /posts 201 312ms
GET /posts/abc-123 404 8ms
PATCH /posts/def-456 200 267msTesting
npm test # run all tests once
npm run test:watch # watch modeTests live in tests/ and are excluded from the build. 144 tests covering:
- DSL builders and all fake hints (
string(),number(),object(),array(), ...) - Zod schema generation for all field types
entity()helpercreateMemoryDB— CRUD, pagination, sorting, search, filter operators, seed, meta, relations, image dimensionsvalidateConfig— all warning scenarios
License
ISC
