Skip to content
Extending/Writing a driver

Writing a driver

Four steps. The compiler enforces the last one, because DRIVERS is a closed Record<DriverEnum, DriverDefinition> — add an enum member without registering it and the build fails.

src/enums/driver.enum.ts
export enum DriverEnum {
// …
MY_BACKEND = 'my-backend',
}

Extend AbstractRequestStrategy and override one hook. buildUri() handles resource assertion and joining; parts() returns the query-string segments.

src/strategies/my-backend-request.strategy.ts
import { AbstractRequestStrategy } from '@qubeejs/core';
export class MyBackendRequestStrategy extends AbstractRequestStrategy {
public readonly capabilities: StrategyCapabilities = {
embedded: false,
fields: false,
filters: true,
includes: false,
operatorFilters: false,
search: false,
select: true,
sort: true,
};
protected parts(state: QueryBuilderState, options: QueryBuilderOptions): string[] {
const out: string[] = [];
Object.entries(state.filters).forEach(([field, values]) => {
out.push(`${field}=${values.join(',')}`);
});
out.push(`${options.page}=${state.page}`, `${options.limit}=${state.limit}`);
return out;
}
}

Declare capabilities honestly. It is what makes the builder throw a useful error instead of emitting something the server ignores.

Leave state.params alone. QueryBuilder.generateUri() appends those after buildUri() returns, for every strategy, and throws ParamCollisionError if one names a key your strategy emitted.

If the envelope is flat, extend AbstractFlatResponseStrategy and you are done — the base reads every field by name from ResponseOptions:

export class MyBackendResponseStrategy extends AbstractFlatResponseStrategy {}

If it is nested, extend AbstractDotPathResponseStrategy, which resolves dotted paths like meta.pagination.total. If it is neither, implement IResponseStrategy directly.

src/drivers/my-backend.driver.ts
export const MY_BACKEND_DRIVER: DriverDefinition = {
id: 'my-backend',
createRequestStrategy: () => new MyBackendRequestStrategy(),
createResponseStrategy: () => new MyBackendResponseStrategy(),
createResponseOptions: (config) => new ResponseOptions(config),
};

Then one line in DRIVERS, and a named export from src/index.ts.

The four steps above add a driver to this repository. A driver can equally live in your own application: export a DriverDefinition and pass it to createQubee(). Give it an id of its own so capability errors name it, rather than the built-in driver it started from:

src/api/studio-api.driver.ts
import {
createQubee,
JsonApiRequestStrategy,
JsonApiResponseOptions,
JsonApiResponseStrategy,
type DriverDefinition,
} from '@qubeejs/core';
class StudioApiRequestStrategy extends JsonApiRequestStrategy {
// override what this backend does differently
}
export const STUDIO_API_DRIVER: DriverDefinition = {
id: 'studio-api',
createRequestStrategy: () => new StudioApiRequestStrategy(),
createResponseStrategy: () => new JsonApiResponseStrategy(),
createResponseOptions: (config) => new JsonApiResponseOptions(config),
};
const { builder } = createQubee({ driver: STUDIO_API_DRIVER });
builder.setSearch('jazz');
// UnsupportedSearchError: The 'studio-api' driver does not support full-text search.

id is a DriverId: any string, while editors still suggest the twenty built-in ids. Only those resolve through DRIVERS, so a driver of your own is always passed as its definition.

Use the bundled stringify() rather than URLSearchParams, which percent-encodes brackets with no opt-out:

import { stringify } from '@qubeejs/core';
stringify({ filters: { status: { $eq: 'published' } } });
// filters[status][$eq]=published

stringify() does not encode, and does not need to: AbstractRequestStrategy.buildUri() hands parts() a state whose filter values, operator values and search term are already percent-encoded. Emit them as they arrive. Encoding them again produces %2520 where a space should be, and anything your strategy adds around a value — operators, brackets, a joining , — must stay literal.