@go-denki/fraft
v0.2.2
Published
Dynamically fetch or send API data using JSON/YAML config with a built-in transform pipeline
Maintainers
Readme
fraft
Declarative API fetching for Node.js. Define HTTP requests and data transformations in a JSON or YAML config file, then execute them with a single method call.
# api.yaml
version: 1
baseUrl: https://jsonplaceholder.typicode.com
requests:
todos:
path: /todos
transform:
- filter: { field: completed, op: eq, value: true }
- pick: [id, title]import { FraftClient } from '@go-denki/fraft';
const client = new FraftClient({ config: 'api.yaml' });
const todos = await client.run('todos');Table of Contents
- Installation
- Quick Start
- Config Format
- API Reference
- Environment Variable Interpolation
- Custom Middleware
- Examples
Installation
npm install @go-denki/fraftQuick Start
1. Create a config file (api.yaml or api.json):
version: 1
baseUrl: https://api.example.com
headers:
Accept: application/json
auth:
type: apiKey
in: header
name: x-api-key
value: "${env.API_KEY}"
requests:
users:
path: /users
transform:
- filter: { field: active, op: eq, value: true }
- pick: [id, name, email]2. Run it:
import { FraftClient } from '@go-denki/fraft';
const client = new FraftClient({ config: 'api.yaml' });
const users = await client.run('users');
console.log(users);Config Format
Config files can be YAML (.yaml / .yml) or JSON (.json). You can also pass a plain JavaScript object directly to FraftClient.
Top-level fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| version | number | No | Schema version. Currently must be 1 if provided. |
| baseUrl | string | Yes | Base URL prepended to all request paths. |
| headers | Record<string, string> | No | Global headers merged into every request. |
| auth | AuthConfig | No | Global auth applied to every request. |
| requests | Record<string, RequestDef> | Yes | Named request definitions. |
Request definition
Each key under requests maps to a RequestDef:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| path | string | — | URL path appended to baseUrl. |
| method | HttpMethod | "GET" | HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. |
| headers | Record<string, string> | — | Per-request headers, merged over global headers. |
| params | Record<string, unknown> | — | Query string parameters. |
| body | unknown | — | Request body (serialized to JSON). |
| transform | TransformStep[] | — | Ordered list of transform steps applied to the response. |
| axiosConfig | AxiosRequestConfig | — | Advanced axios options (timeout, responseType, etc.). |
Example — POST with a body:
requests:
createUser:
path: /users
method: POST
headers:
Content-Type: application/json
body:
name: Alice
role: adminExample — GET with query params:
requests:
search:
path: /search
params:
q: typescript
limit: 10Auth
Currently supports API key auth (type: apiKey).
| Field | Type | Description |
|-------|------|-------------|
| type | "apiKey" | Auth strategy. |
| in | "header" | "query" | Where to inject the key. |
| name | string | Header name or query param key. |
| value | string | Key value. Supports ${env.VAR_NAME} interpolation. |
auth:
type: apiKey
in: header
name: Authorization
value: "Bearer ${env.ACCESS_TOKEN}"Transform pipeline
transform is an ordered array of steps. Each step is applied to the output of the previous one. Steps operate on the full response body (object or array).
pick — keep only selected fields
transform:
- pick: [id, name, email]Applied to an array, picks fields from every element. Applied to an object, picks fields from that object.
rename — rename fields
transform:
- rename:
userId: id
userName: nameMaps old_name → new_name. Works on arrays and single objects.
filter — filter array items
transform:
- filter:
field: status
op: eq
value: activeOnly works on arrays. Supported operators:
| op | Description |
|------|-------------|
| eq | Strict equal (===) |
| neq | Not equal (!==) |
| gt | Greater than |
| gte | Greater than or equal |
| lt | Less than |
| lte | Less than or equal |
| contains | String contains substring |
| startsWith | String starts with value |
| endsWith | String ends with value |
coerce — type coercion and arithmetic
transform:
- coerce:
price:
type: number
expr: "*100"
format: "%.2f"
active: boolean
code: stringEach key maps to a field name. The value can be:
- A shorthand string:
"string","number", or"boolean" - A rule object:
| Field | Type | Description |
|-------|------|-------------|
| type | "string" | "number" | "boolean" | Cast the value to this type. |
| expr | string | Arithmetic applied after type cast: "+5", "-1", "*100", "/1000". |
| format | string | sprintf-style format applied last. Only "%.Nf" (fixed decimal) is supported. |
middleware — custom transform function
transform:
- middleware: myTransformCalls a named function registered via client.use(). See Custom Middleware.
API Reference
FraftClient
import { FraftClient } from '@go-denki/fraft';new FraftClient(options)
| Option | Type | Description |
|--------|------|-------------|
| config | string \| Record<string, unknown> | Path to a JSON/YAML file, or a pre-parsed config object. |
| axiosInstance | AxiosInstance | Optional custom axios instance (useful for testing or interceptors). |
client.run(requestName, overrides?)
Executes a single named request and returns the transformed response.
const data = await client.run('users');
// With runtime overrides:
const data = await client.run('users', {
params: { limit: 5 },
headers: { 'X-Trace-Id': '123' },
});
// With path params:
const user = await client.run('getUser', { pathParams: { id: 42 } });
// path: '/users/:id' → GET /users/42Overrides (RunOverrides):
| Field | Type | Description |
|-------|------|-------------|
| pathParams | Record<string, string \| number> | Values interpolated into :param segments of the request path. |
| params | Record<string, unknown> | Merged over config-defined params. |
| body | unknown | Replaces the config-defined body. |
| headers | Record<string, string> | Merged over config-defined headers. |
Returns Promise<unknown> — the raw or transformed response data.
client.runAll(overrides?)
Executes all requests defined in the config sequentially.
const results = await client.runAll();
// { users: [...], posts: [...] }
// With per-request overrides:
const results = await client.runAll({
users: { params: { limit: 10 } },
});Returns Promise<Record<string, unknown>> — a map of requestName → result.
client.use(name, fn)
Registers a named middleware function for use in { middleware: "<name>" } transform steps.
client.use('addTimestamp', (data) => {
return { ...(data as object), fetchedAt: new Date().toISOString() };
});The middleware function receives (data: unknown, context: RequestContext) and may return a value or a Promise. Returns this for chaining.
client.getConfig()
Returns the loaded and validated FraftConfig object (loads from file on first call, then cached).
Types
All types are exported from the package root:
import type {
FraftConfig,
RequestDef,
TransformStep,
PickStep,
RenameStep,
CoerceStep,
CoerceRule,
FilterStep,
MiddlewareStep,
AuthConfig,
ApiKeyAuth,
HttpMethod,
RunOverrides,
RequestContext,
MiddlewareFn,
FraftClientOptions,
} from '@go-denki/fraft';Environment Variable Interpolation
Any string value in the config can reference an environment variable using the syntax ${env.VAR_NAME}. The variable is resolved at load time from process.env.
auth:
type: apiKey
in: header
name: x-api-key
value: "${env.MY_API_KEY}"
baseUrl: "${env.API_BASE_URL}"If the referenced variable is not set, fraft throws an error at startup.
Custom Middleware
Middleware lets you run arbitrary JavaScript/TypeScript logic as a transform step.
# api.yaml
requests:
posts:
path: /posts
transform:
- pick: [id, title, body]
- middleware: enrichPostsimport { FraftClient } from '@go-denki/fraft';
const client = new FraftClient({ config: 'api.yaml' });
client.use('enrichPosts', async (data, context) => {
const posts = data as Array<{ id: number; title: string; body: string }>;
return posts.map(post => ({
...post,
url: `https://example.com/posts/${post.id}`,
wordCount: post.body.split(' ').length,
}));
});
const posts = await client.run('posts');The context argument provides:
| Field | Type | Description |
|-------|------|-------------|
| requestName | string | The key of the current request. |
| config | FraftConfig | The full parsed config. |
| def | RequestDef | The current request definition. |
Examples
See the examples/ directory for ready-to-run examples:
| Example | Description |
|---------|-------------|
| examples/github-repos.yaml | Fetch public GitHub repos with filtering and field picking |
| examples/weather.yaml | Weather API with API key auth via env var |
| examples/create-post.yaml | POST request with a JSON body |
| examples/basic.mjs | Simple programmatic usage (ESM) |
| examples/transforms.mjs | Demonstrates all built-in transform steps |
| examples/middleware.mjs | Custom middleware transform |
| examples/inline-config.mjs | Passing a config object instead of a file path |
Roadmap / Todo
Auth
- [ ] Bearer / JWT auth type
- [ ] HTTP Basic auth support
- [ ] Auth token refresh and retry on 401
- [ ] Dynamic auth value resolution at request time
Transforms
- [ ]
mapstep — apply a transform to every element of an array - [ ]
sortstep — sort arrays by field - [ ]
flatten/unflattensteps - [ ] Conditional transforms — skip a step based on data shape
- [ ] Dot-notation support for nested field access (
user.profile.name) - [ ] Additional
filteroperators — regex,in(array membership), nested paths - [ ] Additional
coerceformats — dates, locales, string templates - [ ] Error recovery in pipelines — fallbacks on step failure
Config & Environment
- [ ] Default values for env vars —
${env.VAR:default} - [ ] Config composition / inheritance — extend a base config
- [ ] Environment-specific config files (
api.dev.yaml,api.prod.yaml) - [ ] Built-in
.envfile loading via dotenv
Reliability
- [ ] Retry logic with configurable backoff (useful for 429 / 5xx)
- [ ] Per-request timeout override at the top level
- [ ] Partial failure mode for
runAll()— continue on individual request errors - [ ] Structured error types (distinguish auth errors, network errors, transform errors)
HTTP & Requests
- [ ] Multipart /
form-datarequest body support - [ ] Request and response interceptor hooks
- [ ] Streaming response support
- [x] Expand
RunOverridesto allow path params (:paramsyntax) — method and transform overrides still pending
Middleware
- [ ] Global middleware — registered middleware runs on every request
- [ ] Middleware execution ordering / priority
- [ ] Ability to update or unregister middleware entries
- [ ] Built-in utility middleware (CSV parsing, pagination handling, etc.)
Performance
- [ ] Parallel execution mode for
runAll() - [ ] Client-side response caching with TTL
- [ ] Request deduplication
Testing & DX
- [ ] Test utilities exported for consumers to test their own configs
- [ ] Config validation CLI (
fraft validate api.yaml) - [ ] Config loader unit tests
License
MIT
