npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yrest/cli

v0.12.0

Published

YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.

Readme

npm version npm downloads license CI Node TypeScript Socket

YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.

Think json-server, but powered by YAML — with relations, filters, pagination, nested routes, snapshots and custom handlers.

# db.yml
users:
  - id: 1
    name: Ana
    email: [email protected]

posts:
  - id: 1
    title: First post
    userId: 1
npx @yrest/cli serve db.yml
GET  /users          → [{ id: 1, name: "Ana", email: "[email protected]" }]
GET  /users/1        → { id: 1, name: "Ana", email: "[email protected]" }
POST /users          → 201 Created
PUT  /users/1        → 200 OK
PATCH /users/1       → 200 OK
DELETE /users/1      → 200 OK

Why yRest?

A YAML-first alternative to json-server for frontend development.

| Feature | yrest | json-server | | --------------------------------------------------- | :---: | :---------: | | YAML database | ✅ | ❌ | | Zero config | ✅ | ✅ | | Full CRUD | ✅ | ✅ | | Field operators (_gte, _like, _regex…) | ✅ | ⚠️ | | Full-text search | ✅ | ✅ | | Relations: many2one, one2one, many2many | ✅ | ⚠️ | | Nested routes + bidirectional many2many | ✅ | ✅ | | Auto-embedding (nested: true) | ✅ | ❌ | | Field projection (_fields) | ✅ | ❌ | | Pageable mode (envelope response) | ✅ | ❌ | | Custom static routes (_routes) | ✅ | ❌ | | Template variables in responses | ✅ | ❌ | | Handler functions (JS logic) | ✅ | ❌ | | Conditional scenarios (scenarios:) | ✅ | ❌ | | Snapshot endpoints | ✅ | ❌ | | Config file | ✅ | ⚠️ | | API overview page (/_about) | ✅ | ❌ | | Watch mode | ✅ | ✅ | | Readonly mode | ✅ | ❌ | | Atomic writes | ✅ | ✅ | | TypeScript types | ✅ | ❌ | | Programmatic API for test frameworks | ✅ | ❌ | | OpenAPI 3.0 spec (GET /_openapi, yrest openapi) | ✅ | ❌ | | Field annotations (_schema) | ✅ | ❌ | | Compact relation DSL (m2o:target@fk[card]+nested) | ✅ | ❌ | | SSE stream routes (_method: SSE) | ✅ | ❌ |


Install

npm install -D @yrest/cli

Or run directly with npx (no install needed):

npx @yrest/cli serve db.yml

Quick start

# Create a sample db.yml and yrest.config.yml
npx @yrest/cli init

# Start the server
npx @yrest/cli serve db.yml
yrest  ·  http://localhost:3070

Collections (base: /):
  CRUD  /users
  CRUD  /posts

Meta:
  GET    /_about

Open http://localhost:3070/_about for a live overview of all generated endpoints, active modes and ready-to-run curl examples.


Commands

init

Creates a sample db.yml and a yrest.config.yml template in the current directory.

npx @yrest/cli init                            # basic sample (default)
npx @yrest/cli init --sample relational        # with _rel relations
npx @yrest/cli init --file api.yml             # custom filename
npx @yrest/cli init --sample relational --file api.yml

| Flag | Default | Description | | ---------- | -------- | ------------------------------------------------ | | --file | db.yml | Output filename | | --sample | basic | Sample data (basic, relational, ecommerce) |

Samples:

  • basic — three independent collections: users, products and categories
  • relational — blog domain with all three relation types: users, posts, comments, tags and a pivot table
  • ecommerce — e-commerce domain with products, orders, reviews and custom _routes (scenarios, template vars, error injection)

openapi

Generates an OpenAPI 3.0.3 spec from a db.yml file without starting a server.

npx @yrest/cli openapi db.yml                          # writes openapi.yaml
npx @yrest/cli openapi db.yml --format json            # writes openapi.json
npx @yrest/cli openapi db.yml --stdout                 # prints to stdout
npx @yrest/cli openapi db.yml --output api-spec.yaml   # custom output path

| Flag | Default | Description | | ----------------- | ----------- | ----------------------------------------------------------- | | --output <file> | (auto) | Output file path (default: openapi.yaml / openapi.json) | | --format <fmt> | yaml | Output format: yaml or json | | --stdout | false | Print to stdout instead of writing a file | | --base <base> | (none) | Base path prefix applied to all routes | | --port <port> | 3070 | Server port shown in the servers block | | --host <host> | localhost | Server host shown in the servers block | | --title <title> | yRest API | API title for the info block |

The spec is also available live at GET /_openapi (YAML) and GET /_openapi.json (JSON) while the server is running — no extra flag or config needed.


serve

Starts the mock server.

npx @yrest/cli serve db.yml
npx @yrest/cli serve db.yml --port 3001 --host 0.0.0.0
npx @yrest/cli serve db.yml --base /api --watch
npx @yrest/cli serve db.yml --readonly --delay 300
npx @yrest/cli serve db.yml --pageable 20
npx @yrest/cli serve db.yml --snapshot
npx @yrest/cli serve db.yml --handlers yrest.handlers.js

| Flag | Default | Description | | ------------------- | ----------- | ----------------------------------------------------------------------- | | --port | 3070 | Port to listen on | | --host | localhost | Host to bind | | --base | (none) | Prefix for all routes (e.g. /api) | | --watch | false | Reload db.yml automatically when it changes on disk | | --readonly | false | Reject all write operations (POST, PUT, PATCH, DELETE) with 405 | | --delay <ms> | 0 | Add a fixed delay to all responses (simulates network latency) | | --pageable [n] | false | Wrap GET collection responses in { data, pagination }. Optional limit | | --snapshot | false | Save initial state snapshot and expose /_snapshot endpoints | | --handlers <file> | (none) | Path to a JS file exporting handler functions for custom routes | | --id-strategy | increment | Id generation for new items: increment (auto-int) or uuid |

All flags can also be set in yrest.config.yml (see below). CLI flags always take priority over the config file.


Configuration file

yrest init creates a yrest.config.yml alongside db.yml. Options defined here apply every time you run serve without needing to type flags:

# yrest.config.yml
file: db.yml
port: 3070
host: localhost
# base: /api
# watch: false
# readonly: false
# delay: 0
# pageable: false   # true (limit 10), or a number (custom limit)
# snapshot: false
# handlers: yrest.handlers.js
# idStrategy: increment   # increment or uuid

Priority order (highest wins): CLI flags → yrest.config.yml → schema defaults.


Database format

users:
  - id: 1
    name: Ana
    email: [email protected]
  - id: 2
    name: Luis
    email: [email protected]

posts:
  - id: 1
    title: First post
    userId: 1

Each top-level key becomes a resource with full CRUD endpoints.

Alternatively, collections can be grouped under a _data block:

_data:
  users:
    - id: 1
      name: Ana
  posts:
    - id: 1
      title: First post
      userId: 1

Both formats are equivalent. You can also mix them — if the same collection name appears both inside _data and at the root level, the root-level entry wins. The file is persisted in whichever format it was originally written.


Field annotations (_schema)

Add a _schema block to db.yml to declare field-level metadata per collection. Used by the OpenAPI generator to produce accurate schemas — has no effect on runtime CRUD behavior.

All keys inside _schema field objects use the _ prefix convention. Two shorthand strings are also accepted at the field level:

_schema:
  users:
    name: required # shorthand → { _required: true }
    email:
      _required: true
      _type: string
      _format: email
      _minLength: 5
      _maxLength: 100
    age:
      _type: integer
      _minimum: 0
      _maximum: 150
      _nullable: true
    role:
      _type: string
      _enum: [admin, editor, viewer]
      _default: viewer
      _description: User role
    tags:
      _type: array
      _items:
        _type: string
      _minItems: 1
      _uniqueItems: true
    createdAt:
      _type: string
      _format: date-time
      _readOnly: true

users:
  - id: 1
    name: Ana
    email: [email protected]
    age: 28
    role: admin

Core

| Key | Type | Description | | -------------- | --------- | ----------------------------------------------------------------------------------------- | | _required | boolean | Marks the field as required in the OpenAPI schema | | _type | string | Overrides the inferred type (string, integer, number, boolean, array, object) | | _format | string | OpenAPI format hint (email, date, date-time, uuid, uri, …) | | _enum | array | Restricts the field to a fixed set of values | | _description | string | Field description included in the OpenAPI schema | | _default | any | Default value shown in the OpenAPI schema | | _example | any | Example value shown in the OpenAPI schema | | _nullable | boolean | Allows the field to be null |

String constraints

| Key | Type | Description | | ------------ | -------- | ------------------------------------ | | _minLength | number | Minimum string length | | _maxLength | number | Maximum string length | | _pattern | string | Regex pattern the value must satisfy |

Number / integer constraints

| Key | Type | Description | | ------------------- | -------- | ----------------------------- | | _minimum | number | Inclusive lower bound | | _maximum | number | Inclusive upper bound | | _exclusiveMinimum | number | Exclusive lower bound | | _exclusiveMaximum | number | Exclusive upper bound | | _multipleOf | number | Value must be a multiple of N |

Array constraints

| Key | Type | Description | | -------------- | --------- | ------------------------------------------------------------ | | _minItems | number | Minimum number of array items | | _maxItems | number | Maximum number of array items | | _uniqueItems | boolean | All items must be unique | | _items | object | Schema for array items — accepts _type, _format, _enum |

OpenAPI meta

| Key | Type | Description | | ------------- | --------- | --------------------------------------- | | _deprecated | boolean | Marks the field as deprecated | | _readOnly | boolean | Field is read-only (excluded from POST) | | _writeOnly | boolean | Field is write-only (excluded from GET) |

Fields not listed in _schema are inferred from the first items in the collection and treated as optional. _schema itself is excluded from the generated REST resources.


Generated endpoints

For each resource in db.yml:

GET     /users          List all
GET     /users/:id      Get one
POST    /users          Create
PUT     /users/:id      Replace
PATCH   /users/:id      Partial update
DELETE  /users/:id      Delete

With --base /api all routes are prefixed: /api/users, /api/users/:id, etc.


Query params

All query params can be combined freely.

Filtering

Return only items that match one or more field values:

GET /users?name=Ana
GET /users?role=admin&active=true

Comparison is case-sensitive and converts types to string (?id=1 matches numeric id: 1).

Repeated params are treated as OR — any match passes:

GET /users?role=admin&role=editor    # returns admins and editors

Field operators

Append an operator suffix to any field name:

GET /users?age_gte=18               # age >= 18
GET /users?age_lte=65               # age <= 65
GET /users?status_ne=inactive       # status != "inactive"
GET /users?name_like=ana            # name contains "ana" (case-insensitive)
GET /users?name_start=A             # name starts with "A" (case-insensitive)
GET /users?email_regex=@gmail\.com  # email matches regex (case-insensitive)

| Suffix | Type | Description | | -------- | ---------------- | -------------------------------- | | _gte | numeric / string | Greater than or equal | | _lte | numeric / string | Less than or equal | | _ne | any | Not equal | | _like | string | Case-insensitive substring match | | _start | string | Case-insensitive prefix match | | _regex | string | Case-insensitive regex match |

Full-text search

Search across all scalar fields of every item (case-insensitive substring match):

GET /users?_q=ana
GET /posts?_q=javascript

An item passes if any string or number field contains the search term.

Sorting

GET /users?_sort=name              # ascending (default)
GET /users?_sort=name&_order=desc  # descending

String fields are compared case-insensitively. Items missing the sort field are pushed to the end.

Pagination

Without --pageable (default):

GET /users?_page=1&_limit=10   # page 1, 10 items per page
GET /users?_limit=5            # first 5 items

When _page or _limit are used, the response includes an X-Total-Count header with the total number of items before pagination.

With --pageable (or pageable: true in config):

Every GET collection response is automatically wrapped in a { data, pagination } envelope:

npx @yrest/cli serve db.yml --pageable      # default limit: 10
npx @yrest/cli serve db.yml --pageable 20   # custom limit: 20
{
  "data": [
    { "id": 1, "name": "Ana" },
    { "id": 2, "name": "Luis" }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "totalItems": 23,
    "totalPages": 3,
    "isFirst": true,
    "isLast": false,
    "hasNext": true,
    "hasPrev": false
  }
}

The ?_page and ?_limit query params still work in pageable mode to navigate pages.

Field projection

Return only specific fields in the response:

GET /users?_fields=id,name
GET /posts?_fields=id,title,userId

Works on both collection and single-item endpoints.

Relation embedding (_expand)

Embed a related parent object directly into the response using the _rel block (see Relational data):

GET /posts?_expand=user           # embed user object in each post
GET /posts/1?_expand=user         # embed in a single item

Both syntaxes are supported:

?_expand=author,category          # comma-separated
?_expand=author&_expand=category  # repeated param

Unresolvable keys are silently ignored. Works on all operations: GET, POST, PUT, PATCH, DELETE.

Embed children (_embed)

Embed related child collections directly into a parent item:

GET /users/1?_embed=posts         # embed all posts where userId === 1
GET /users?_embed=posts           # embed posts in every user

Both syntaxes are supported:

?_embed=posts,comments            # comma-separated
?_embed=posts&_embed=comments     # repeated param

Requires _rel to be declared (see Relational data).

Combined example

GET /posts?userId=1&_sort=title&_order=asc&_page=1&_limit=5&_expand=user&_fields=id,title,user

Returns the first 5 posts by user 1, sorted alphabetically by title, with the user object embedded, returning only id, title and user fields.


Relational data

Use _rel to declare relationships between collections. Three relation types are supported.

many2one (default)

A child record holds a foreign key pointing to a parent. The string shorthand implicitly declares many2one:

_rel:
  posts:
    userId: users # shorthand: many2one

one2one

One child record belongs to exactly one parent, and a parent has at most one child:

_rel:
  profiles:
    userId:
      _type: one2one
      _target: users

many2many

Two collections are linked through a pivot (join) table:

_rel:
  posts:
    tags:
      _type: many2many
      _target: tags
      _through: post_tags # pivot collection
      _foreignKey: postId
      _otherKey: tagId

post_tags:
  - id: 1
    postId: 1
    tagId: 2

Auto-embedding with _nested: true

Add _nested: true to any relation to embed the related data automatically in every GET response, without needing ?_expand or ?_embed:

_rel:
  posts:
    userId:
      _type: many2one
      _target: users
      _nested: true # user object embedded in every /posts response
    tags:
      _type: many2many
      _target: tags
      _through: post_tags
      _foreignKey: postId
      _otherKey: tagId
      _nested: true # tags array embedded in every /posts response
GET /posts/1  →  { id: 1, title: "...", userId: 1, user: { ... }, tags: [...] }

many2one / one2one with _nested: true embed the parent object under the derived key (userIduser). The original FK field is preserved. many2many with _nested: true embeds the resolved target array under the alias key.

Compact DSL syntax

Relations can also be declared using a compact string format — useful when you want to express type, target, foreign key and cardinality in a single line:

_rel:
  payments:
    bookingId: "m2o:bookings[1..1->0..n]" # type:target[carDirect->carInverse]
    userId: "m2o:users[1..1->0..n]+nested" # +nested flag auto-embeds on every GET

  profiles:
    userId: "o2o:users[1..1->1..1]"

  posts:
    authorId: "m2o:users@userId[1..1->0..n]" # @foreignKey when field name ≠ FK column

  tags:
    posts: "m2m:posts@post_tags(postId,tagId)[0..n->0..n]" # m2m with pivot

Type aliases:

| Alias | Full name | | ----- | ----------- | | m2o | many2one | | o2o | one2one | | m2m | many2many |

Cardinality notation: [carDirect->carInverse] — format min..max where min is 0 or 1 and max is 1 or n. Both segments are optional: omitting them applies defaults (1..1->1..n for m2o, 1..1->1..1 for o2o, 0..n->0..n for m2m).

+nested flag: appended at the end of any DSL string, equivalent to _nested: true.

The three forms — shorthand string, compact DSL, and verbose object — are fully interchangeable and can be mixed freely within the same _rel block.

What relations enable

Nested routes (many2one / one2one):

GET /users/1/posts         # all posts where userId === 1
GET /users/1/posts/3       # post 3 only if it belongs to user 1
GET /users/1/profiles      # profile for user 1 (one2one: returns object, not array)

Nested routes (many2many — bidirectional):

Both directions are registered automatically from a single declaration:

GET /posts/1/tags          # all tags linked to post 1 via pivot
GET /tags/2/posts          # all posts linked to tag 2 via pivot (inverse — auto-created)

Embed parent with ?_expand:

GET /posts/1?_expand=user   →  { id: 1, title: "First post", userId: 1, user: { id: 1, name: "Ana" } }

Embed children with ?_embed:

GET /users/1?_embed=posts          →  { id: 1, name: "Ana", posts: [...] }
GET /posts/1?_embed=tags           →  { id: 1, title: "...", tags: [...] }   # many2many
GET /users/1?_embed=profiles       →  { id: 1, name: "Ana", profiles: { ... } }  # one2one: object

Custom routes

Define endpoints that don't fit CRUD directly in db.yml using the _routes block.

Static responses

_routes:
  - method: POST
    path: /login
    response:
      status: 200
      body:
        token: fake-jwt-token-abc123

  - method: POST
    path: /logout
    response:
      status: 204

  - method: GET
    path: /dashboard/stats
    response:
      status: 200
      headers:
        Cache-Control: no-store
      body:
        users: 150
        revenue: 4820.50
  • method is case-insensitive. path supports Fastify params (:id).
  • response.status defaults to 200. response.body is any YAML value.
  • Custom routes are registered before resource routes and always take priority.
  • Shown in /_about under "Custom routes".

Template variables

Interpolate request data into the response body using {{}} syntax:

_routes:
  - method: GET
    path: /users/:id/summary
    response:
      status: 200
      body:
        requestedId: "{{params.id}}"
        timestamp: "{{now}}"

  - method: POST
    path: /echo
    response:
      status: 200
      body:
        received: "{{body}}"
        query: "{{query}}"
        requestId: "{{uuid}}"

Available variables:

| Variable | Description | | --------------- | --------------------------------------------------- | | {{params.X}} | URL parameter (e.g. {{params.id}}) | | {{query.X}} | Query string param (e.g. {{query.page}}) | | {{body}} | Full request body | | {{body.X}} | Field from the request body (e.g. {{body.email}}) | | {{headers.X}} | Request header value | | {{now}} | Current UTC timestamp (ISO 8601) | | {{uuid}} | Random UUID v4 |

When a field contains only a single {{variable}} placeholder, the resolved value preserves its original type (number, boolean, object). When embedded in a larger string it is stringified.

Conditional scenarios

Define multiple conditional response variants for a custom route. Scenarios are evaluated in declaration order — the first matching when: wins. If none match, the otherwise: block is used (if defined), otherwise the static response: block.

_routes:
  - method: POST
    path: /login
    scenarios:
      - when:
          body.email: [email protected]
          body.password: secret
        response:
          status: 200
          body:
            token: tok-ana
      - when:
          body.email: [email protected]
          body.password: admin
        response:
          status: 200
          body:
            token: tok-admin
            role: admin
    otherwise:
      status: 401
      body:
        error: Invalid credentials

when: as an object — all entries must match (AND semantics):

when:
  body.email: [email protected]
  body.password: secret

when: as an array of objects — any group satisfying all its conditions matches (OR of ANDs):

when:
  - body.role: admin
  - body.role: superadmin

Condition keys use dot-notation to address request data. Both bare and _-prefixed forms are accepted:

| Prefix | Example | Resolves to | | -------------------------- | ------------------- | -------------------------- | | body.X / _body.X | body.email | req.body.email | | params.X / _params.X | params.id | req.params.id | | query.X / _query.X | query.page | req.query.page | | headers.X / _headers.X | headers.x-api-key | req.headers["x-api-key"] |

Field operator suffixes (_ne, _like, _start, _regex, _gte, _lte) work on condition keys exactly as they do on query params:

scenarios:
  - when:
      body.name_like: ana # name contains "ana" (case-insensitive)
      body.age_gte: "18" # age >= 18
    response:
      status: 200
      body: { ok: true }

Template variables ({{}}) are supported in both scenario and otherwise response bodies:

scenarios:
  - when:
      body.email: [email protected]
    response:
      status: 200
      body:
        message: "Welcome {{body.email}}"
otherwise:
  status: 401
  body:
    error: "Unknown user: {{body.email}}"

Per-route delay

Add a fixed delay (ms) to a specific route without affecting the rest of the server. Takes priority over the global --delay option for that route:

_routes:
  - method: GET
    path: /slow-endpoint
    delay: 800
    response:
      status: 200
      body: { data: loaded }

Error injection

Force a custom route to always return a specific HTTP error, regardless of handlers, scenarios or the static response. Useful for simulating outages, payment failures or auth errors:

_routes:
  - method: GET
    path: /payments
    error: 503
    errorBody:
      message: Service temporarily unavailable
      retryAfter: 30

  - method: POST
    path: /checkout
    error: 402
    errorBody:
      error: Payment required

  - method: GET
    path: /slow-failure
    delay: 400
    error: 500
  • error takes priority over handler, scenarios and response — the route always returns this status.
  • errorBody is optional. If omitted, the default body is { "error": "Forced error NNN" }.
  • delay still applies before the error is returned.
  • Shown in /_about with a red error·NNN badge.

Handler functions

For routes that need real logic (conditional responses, stateful mocks, request inspection), reference a JavaScript function via the handler: field:

_routes:
  - method: POST
    path: /login
    handler: login
    response: # optional fallback if handler throws
      status: 200
      body: { token: fake }

  - method: GET
    path: /auth/me
    handler: getCurrentUser

Create a yrest.handlers.js file in the same directory as db.yml:

// yrest.handlers.js
export async function login(req) {
  const { email, password } = req.body ?? {};
  if (password !== "secret") return { status: 401, body: { error: "Invalid credentials" } };
  return { status: 200, body: { token: `tok-${email}` } };
}

export async function getCurrentUser(req) {
  return { status: 200, body: { id: 1, name: "Ana", role: "admin" } };
}

Pass the file to the server with --handlers:

npx @yrest/cli serve db.yml --handlers yrest.handlers.js

Handler signature:

type HandlerRequest = {
  params: Record<string, string>;
  query: Record<string, string | string[]>;
  body: unknown;
  headers: Record<string, string | string[]>;
};

type HandlerResponse = {
  status?: number; // defaults to 200
  body?: unknown;
  headers?: Record<string, string>;
};

type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>;

If a named handler is not found in the file, the server returns 501. If the handler throws, it returns 500. If a response: block is defined alongside handler:, it is used as fallback only when the handler itself throws.

SSE streams

Mock Server-Sent Events streams by adding _method: SSE and an _sse block to a _routes entry. Clients connect with a regular GET request and receive a continuous stream of events until the connection is closed.

_routes:
  - _method: SSE
    _path: /events/orders
    _sse:
      _interval: 1500 # ms between frames (default: 1000)
      _loop: true # restart after last event (default: true)
      _repeat: 3 # stop after N full cycles (omit = infinite)
      _events:
        - _event: update
          _data:
            orderId: 1
            status: processing
        - _event: update
          _data:
            orderId: 1
            status: shipped
        - _event: done
          _data:
            orderId: 1
            status: delivered

_sse options:

| Key | Default | Description | | ----------- | ------- | ---------------------------------------------------------------------------------------------------- | | _interval | 1000 | Milliseconds between frames | | _loop | true | Restart the sequence after the last frame. Set to false to close after one cycle | | _repeat | — | Stop after N complete cycles and close the stream. Omit for infinite | | _events | — | Ordered list of frames. Each entry needs _data (required) and optionally _event (SSE event name) |

Template variables — resolved per frame at emit time, so each event gets a fresh value:

_events:
  - _event: tick
    _data:
      ts: "{{now}}" # ISO timestamp, unique per frame
      id: "{{uuid}}" # random UUID v4
      channel: "{{params.channel}}" # from URL params
      filter: "{{query.filter}}" # from query string

Consuming from the browser:

const es = new EventSource("http://localhost:3070/events/orders");

es.addEventListener("update", (e) => console.log(JSON.parse(e.data)));
es.addEventListener("done", (e) => {
  console.log(JSON.parse(e.data));
  es.close();
});

Consuming from the terminal:

curl -N http://localhost:3070/events/orders

The -N flag disables curl's output buffering, which is required to see SSE frames as they arrive.

A keep-alive comment (: ping) is sent every 15 s to prevent proxy timeouts. SSE routes are shown in /_about under their own SSE streams accordion with a sky-blue SSE badge.


Server modes

Watch mode

Automatically reloads db.yml when it changes on disk — useful when you edit the file manually while the server is running:

npx @yrest/cli serve db.yml --watch

Note: Watch mode reloads data in existing collections. Adding or removing entire collections requires a server restart.

Readonly mode

Rejects all write operations with 405 Method Not Allowed:

npx @yrest/cli serve db.yml --readonly

Useful to expose a stable read-only snapshot for demos or CI environments.

Delay mode

Adds a fixed delay (in milliseconds) to every response to simulate real network latency:

npx @yrest/cli serve db.yml --delay 500   # 500ms on every response

Snapshot mode

Saves the initial database state at startup and exposes three meta endpoints to inspect, save and restore it:

npx @yrest/cli serve db.yml --snapshot

| Endpoint | Description | | ----------------------- | ------------------------------------------------------------------- | | GET /_snapshot | Returns snapshot metadata (saved time + item counts per collection) | | POST /_snapshot/save | Replaces the snapshot with the current database state | | POST /_snapshot/reset | Restores the database to the last saved snapshot |

Useful for test suites that need a clean reset between runs or demos that need a predictable starting state.


Meta endpoints

Every running server exposes the following meta endpoints without any extra configuration:

| Endpoint | Description | | -------------------- | ---------------------------------------------------------------------------------------------------- | | GET /_about | Self-contained HTML page listing all endpoints, modes, query params and ready-to-run curl examples | | GET /_openapi | OpenAPI 3.0.3 spec in YAML format — regenerated on every request | | GET /_openapi.json | OpenAPI 3.0.3 spec in JSON format |

open http://localhost:3070/_about
curl http://localhost:3070/_openapi.json | npx swagger-ui-watcher -

Both /_about and the OpenAPI spec reflect the live storage state and update automatically in watch mode.


HTTP responses

| Status | When | | ------ | -------------------------------------------------------------------- | | 200 | Successful GET, PUT, PATCH, DELETE | | 201 | Successful POST | | 400 | Invalid or missing request body | | 404 | Resource or id not found | | 405 | Write operation in readonly mode | | 500 | Error reading or writing the YAML file | | 501 | Handler referenced in _routes is not exported by the handlers file |

DELETE returns the deleted item as confirmation.


ID generation

If a POST body does not include an id, yrest assigns the next incremental integer automatically. If the body includes an id, it is respected.

Persistence

All write operations (POST, PUT, PATCH, DELETE) are saved back to db.yml immediately using an atomic write strategy (write to temp file → rename), so data is never corrupted even if the process is interrupted.

CORS

CORS is enabled by default, so you can call the API from any frontend running on a different port without extra configuration.


Frontend usage

// List all
const users = await fetch("http://localhost:3070/users").then((r) => r.json());

// Filter + operators + search
const res = await fetch("http://localhost:3070/users?age_gte=18&name_like=ana&_q=dev");

// Sort + paginate + project fields
const res = await fetch("http://localhost:3070/users?_sort=name&_page=1&_limit=10&_fields=id,name");

// Embed related object (parent)
const post = await fetch("http://localhost:3070/posts/1?_expand=user").then((r) => r.json());
// → { id: 1, title: "...", userId: 1, user: { id: 1, name: "Ana" } }

// Embed children
const user = await fetch("http://localhost:3070/users/1?_embed=posts").then((r) => r.json());
// → { id: 1, name: "Ana", posts: [{ id: 1, title: "First post", userId: 1 }] }

// Create
await fetch("http://localhost:3070/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Carlos", email: "[email protected]" }),
});

// Partial update
await fetch("http://localhost:3070/users/1", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ana Updated" }),
});

// Delete
await fetch("http://localhost:3070/users/1", { method: "DELETE" });

// Custom route with handler
const session = await fetch("http://localhost:3070/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "[email protected]", password: "secret" }),
}).then((r) => r.json());
// → { token: "[email protected]" }

Programmatic API

Use yRest directly inside your test suite — no CLI, no separate process to manage.

Install as a dev dependency:

npm install -D @yrest/cli

createYrestServer

Creates a server instance that you control with start() and stop(). Accepts either inline YAML data or a path to a db.yml file.

import { createYrestServer, yrest } from "@yrest/cli";

Options:

| Option | Type | Default | Description | | ---------- | ----------------- | ----------- | ------------------------------------------------------ | | data | Data | — | Inline data object (use with yrest\...`) | | file |string | — | Path to adb.yml file (dataorfileis required) | |port |number |3070 | TCP port. Use0to get a random available port | |host |string |localhost| Host to bind | |base |string | — | URL prefix for all routes (e.g."/api") | | readonly|boolean |false | Reject all write operations with405 | |delay |number |0 | Fixed delay in ms added to every response | |pageable|boolean|number|false | Wrap GET responses in{ data, pagination }envelope | |snapshot|boolean |false | Enable snapshot endpoints (/_snapshot) | | handlers|string` | — | Path to a JS file exporting handler functions |

Returned handle:

| Member | Description | | --------- | -------------------------------------------------------- | | start() | Starts the server and begins listening | | stop() | Gracefully closes the server | | port | The actual port after start() (useful when port: 0) | | url | Base URL after start() (e.g. http://localhost:49821) |

yrest tagged template literal

Parses inline YAML into a data object. Strips common leading indentation automatically, so you can indent naturally inside your test files. Supports interpolated values.

const data = yrest`
  users:
    - id: 1
      name: Ana
  posts:
    - id: 1
      title: First post
      userId: 1
`;

Vitest example

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createYrestServer, yrest } from "@yrest/cli";

const server = createYrestServer({
  data: yrest`
    users:
      - id: 1
        name: Ana
      - id: 2
        name: Luis
  `,
  port: 0, // random port — no conflicts between parallel tests
  readonly: true,
});

beforeAll(() => server.start());
afterAll(() => server.stop());

describe("users API", () => {
  it("returns all users", async () => {
    const res = await fetch(`${server.url}/users`);
    expect(res.status).toBe(200);
    expect(await res.json()).toHaveLength(2);
  });

  it("returns a single user", async () => {
    const res = await fetch(`${server.url}/users/1`);
    expect(await res.json()).toMatchObject({ name: "Ana" });
  });
});

Playwright example

// tests/api.spec.ts
import { test, expect, beforeAll, afterAll } from "@playwright/test";
import { createYrestServer, yrest } from "@yrest/cli";

const server = createYrestServer({
  data: yrest`
    users:
      - id: 1
        name: Ana
  `,
  port: 0,
  readonly: true,
});

beforeAll(() => server.start());
afterAll(() => server.stop());

test("lists users", async () => {
  const res = await fetch(`${server.url}/users`);
  expect(await res.json()).toHaveLength(1);
});

File-based example

When your test data is too large for inline YAML, point to a file:

const server = createYrestServer({
  file: "./tests/fixtures/db.yml",
  port: 0,
  readonly: true,
});

Use in package.json scripts

{
  "scripts": {
    "mock": "yrest serve db.yml",
    "mock:watch": "yrest serve db.yml --watch",
    "mock:readonly": "yrest serve db.yml --readonly --delay 200"
  }
}

Roadmap

| Feature | Status | | -------------------------------------------------- | ------ | | Full CRUD from db.yml | ✅ | | Field filters, operators, full-text search | ✅ | | Relations, _expand, _embed, nested routes | ✅ | | Pagination, sorting, field projection | ✅ | | Watch, readonly, delay, snapshot modes | ✅ | | Custom routes (_routes) with static responses | ✅ | | Template variables in responses ({{params.id}}) | ✅ | | Handler functions (yrest.handlers.js) | ✅ | | Visual panel (/_panel) | 🔜 | | Programmatic API for Vitest / Playwright | ✅ | | Docker image | 🔜 | | OpenAPI export (yrest openapi db.yml) | ✅ | | VS Code extension with YAML snippets | 🔜 | | Request validation with JSON Schema | 🔜 | | Conditional scenarios (scenarios:, otherwise:) | ✅ | | Per-route delay (delay:) | ✅ | | Error injection (error: in _routes) | ✅ | | Configurable ID strategy (idStrategy) | ✅ | | Explicit relation types (one2one, many2many) | ✅ | | Bidirectional many2many nested routes | ✅ | | Auto-embedding (nested: true in _rel) | ✅ | | SSE stream routes (_method: SSE) | ✅ | | WebSocket mock channels (_method: WS) | 🔜 | | TypeScript handlers + typed collections | 🔜 |


Contributing

Prerequisites

  • Node.js >= 20
  • Task — task runner (brew install go-task / scoop install task / other methods)

Task commands

Run task --list to see all available commands.

Development

| Command | What it does | | ----------------------- | ---------------------------------------------------------------------- | | task test | Runs the full test suite once | | task test:watch | Runs tests in watch mode — reruns on every file change | | task build | Compiles TypeScript to dist/ via tsup | | task dev | Builds once, then starts watch-build + server from dist/ in parallel | | task serve:dist | Builds and starts the server from the local dist/ | | task serve:dist:watch | Builds and starts the server with --watch (reloads db.yml on change) | | task serve:npx | Starts the published npm version (useful to compare against local) | | task serve:npx:watch | Starts the published npm version with --watch | | task preflight | Full pre-push check: format, lint, typecheck and tests in order |

Release

| Command | What it does | | -------------------- | -------------------------------------------------------------- | | task release:patch | Bumps x.x.N, creates a git commit and tag | | task release:minor | Bumps x.N.0, creates a git commit and tag | | task release:major | Bumps N.0.0, creates a git commit and tag | | task publish | Runs tests + build and publishes to npm (requires npm login) |

Development workflow

Day-to-day work:

task test:watch   # keep this running in one terminal
task dev          # keep this running in another terminal

test:watch reruns the suite on every save so you catch regressions immediately. dev rebuilds and serves so you can call the endpoints manually while you work.

Before pushing:

task preflight    # format + lint + typecheck + tests in one command

Good practices:

  • Write tests for every new feature or bug fix before opening a PR.
  • Keep db.yml in a valid state — it is used as the default file when running local servers.
  • dist/ is gitignored and generated at build time; never commit it manually.
  • Version bumps are done with task release:*, not by editing package.json directly — the Task command also creates the git tag that triggers the publish pipeline.

Release workflow

Releases are fully automated via GitHub Actions once a version tag is pushed:

# 1. Make sure everything is clean
task preflight

# 2. Bump the version (choose one)
task release:patch   # bug fixes
task release:minor   # new features, backwards compatible
task release:major   # breaking changes

# 3. Push the commit and the tag
git push && git push --tags

Step 3 triggers two GitHub Actions pipelines automatically:

  • CI — runs on every push to main and on every PR. Executes typecheck + tests on Node 20 and Node 22.
  • Publish — runs only when a v* tag is pushed. Runs tests + build and publishes to npm using Trusted Publishing (OIDC — no tokens stored as secrets).

CI/CD pipelines

push to main / PR open
        │
        ▼
    [CI workflow]
    ├── Node 20: lint + format check + typecheck + tests
    └── Node 22: lint + format check + typecheck + tests

push tag v*
        │
        ▼
    [Publish workflow]
    ├── tests + build  (via prepublishOnly)
    └── npm publish --provenance  (via Trusted Publishing / OIDC, Node 24)

Changelog

See CHANGELOG.md for the full version history.


License

MIT