Skip to content

What is rapiq?

Rapiq (Rest Api Query) gives the two sides of an HTTP API one shared query language for list endpoints: filtering, sorting, pagination, sparse field selection and relation loading, modeled after the JSON:API query parameters.

The caller builds a typed query, sends it as an ordinary URL query string, and the receiving application validates it against an allow-list schema before turning it into a database query. No hand-rolled req.query parsing, no string concatenation, no guessing which parameters a client may touch.

txt
?codec=url-expression&filter=gte(age,'18')&include=realm&sort=-age&page[limit]=25&fields=id,name

The 30-second tour

Caller: build a query against your record type and encode it:

typescript
import { defineQuery } from '@rapiq/core';
import { createURLCodec } from '@rapiq/codec-url';

const query = defineQuery<User>({
    filters: { age: { $gte: 18 } },
    relations: ['realm'],
    sort: '-age',
    pagination: { limit: 25 },
});

const response = await fetch(`/users?${createURLCodec().encode(query)}`);

Receiver: decode it against a schema that says what clients may request, then hand it to your database:

typescript
import { SchemaRegistry, defineSchema } from '@rapiq/core';
import { createURLCodec } from '@rapiq/codec-url';
import { TypeormAdapter } from '@rapiq/adapter-typeorm';

const registry = new SchemaRegistry();
registry.add(defineSchema<User>({
    name: 'user',
    filters: { allowed: ['age', 'name'] },
    relations: { allowed: ['realm'] },
    sort: { allowed: ['age', 'name'] },
    pagination: { maxLimit: 50 },
}));

const query = createURLCodec(registry).decode(req.query, { schema: 'user' });
if (!query) {
    // null for non-object input
    return res.status(400).end();
}

new TypeormAdapter({ queryBuilder }).execute(query);
const [entities, total] = await queryBuilder.getManyAndCount();

Everything a client sends is constrained before it reaches the database. Most parameters follow the schema's drop-vs-throw policy; expression filters reject contract violations precisely.

How it works

A query passes through four stages, and every rapiq package plays exactly one role in one of them:

Caller
BuilddefineQuery<User>({ filters, sort, … })typed input · condition helpers eq / and / or
encode · URLCodec
HTTP?codec=url-expression&filter=gte(age,'18')&include=realm&sort=-age&page[limit]=25
Receiver
decode · URLCodec / parsers
Validate checked against the Schema allow-list; anything not permitted is dropped or rejected
Querythe shared, typed AST: same shape on both sides
execute · accept(visitor)
  1. Build: the caller describes what it wants with defineQuery, typed against the record.
  2. Send: a codec turns the query into an ordinary URL query string, and back.
  3. Validate: the receiver parses against a schema; the allow-list decides what survives.
  4. Execute: an adapter translates the validated query for your backend.

Because the pieces only meet in the Query, they compose freely: swap the wire dialect without touching the database code, add a new backend without touching the parsers, or skip the wire entirely and evaluate a query in memory.

The five parameters

ParameterURL keyWhat it does
FieldsfieldsSelect which resource fields are returned.
FiltersfilterNarrow the collection by conditions.
RelationsincludeLoad related resources alongside the primary one.
SortsortOrder the collection by one or more keys.
PaginationpageLimit and offset the collection.

The package family

Install only what each side of your application needs; @rapiq/core is the shared foundation.

RolePackages
Build & compose@rapiq/core: the query AST, defineQuery, condition helpers, schemas
Parse input@rapiq/parser-simple · @rapiq/parser-expression · @rapiq/parser-mongo
Cross the wire@rapiq/codec-url
Execute@rapiq/adapter-typeorm · @rapiq/adapter-sql · @rapiq/adapter-prisma · @rapiq/adapter-drizzle · @rapiq/adapter-memory

See the package overview for the full map and a "which packages do I need?" guide.

When is rapiq a good fit?

  • Your API exposes list endpoints and clients need to filter, sort, paginate or shape the result.
  • You want the query surface declared, typed and enforced instead of scattered across handlers.
  • You want the same query semantics everywhere: SQL, TypeORM, Prisma, Drizzle, in-memory guards, tests.

It deliberately does not define the response format, replace your ORM, or generate endpoints; it only standardizes what a query is and what a client may ask for.

Version 2

These docs cover the upcoming version 2, which splits the former single rapiq package into focused @rapiq/* packages. The v1 documentation lives on the v1 branch; see Migration from v1.

Next steps

Released under the MIT License.