the-api
v22.11.8
Published
The API - Create Your API in Seconds
Downloads
2,374
Readme
the-api
Examples
import { Routings, TheAPI, middlewares } from 'the-api';
const router = new Routings();
router.get('/data/:id', async (c) => { // hono routing
const { id } = c.req.param(); // get route parameter
c.set('result', { id, foo: 'bar' }); // set response result
});
const theAPI = new TheAPI({ routings: [middlewares.common, router] });
await theAPI.up(); // use with node
// ...or use with bun
// export default await theAPI.upBun();Request and response
curl http://localhost:7788/data/123{
"result": {
"id": "123",
"foo": "bar"
},
"error": false,
"requestTime": 4,
"serverTime": "2026-03-05T13:47:54.709Z"
}DB + CRUD example
After starting the server with files from the example, you can perform CRUD operations on messages. See CRUD operations section for examples. For GET all, you can also use query parameters described in Query parameters for GET all section.
- You need to have PostgreSQL running and
the-apiinstalled. - Then, create
.env,./migrations/20260305134700_create_messages_table.tsand./index.tsfiles with the following content:
.env
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_DATABASE=postgres./migrations/20260305134700_create_messages_table.ts
export const up = (knex) => knex.schema
.createTable('messages', (table) => {
table.increments('id').primary();
table.timestamp('timeCreated').defaultTo(knex.fn.now());
table.timestamp('timeUpdated');
table.integer('warningLevel');
table.string('body').notNullable();
});
export const down = (knex) => knex.schema
.dropTable('messages');./index.ts
import { Routings, TheAPI } from 'the-api';
const router = new Routings({ migrationDirs: ['./migrations'] });
router.crud({ table: 'messages' });
const theAPI = new TheAPI({ routings: [router] });
await theAPI.up();- If you're using Node.js, check that you have
"type": "module"in yourpackage.jsonand start your project withnode --env-file=.env index.js. - If you're using Deno, start with
deno run --allow-net --allow-env --allow-read index.ts. - If you're using Bun, just start with
bun index.ts.
CRUD operations
Create
curl -X POST http://localhost:7788/messages -H "Content-Type: application/json" -d '{"warningLevel": 2, "body": "test message"}'
{
"result": {
"id": 1,
"timeCreated": "2026-03-06T12:52:43.568Z",
"timeUpdated": null,
"warningLevel": 2,
"body": "test message"
},
"error": false,
"requestTime": 34,
"serverTime": "2026-03-06T12:52:43.571Z"
}Update
curl -X PATCH http://localhost:7788/messages/1 -H "Content-Type: application/json" -d '{"warningLevel": 3}'
{
"result": {
"id": 1,
"timeCreated": "2026-03-06T12:52:43.568Z",
"timeUpdated": "2026-03-06T12:52:47.350Z",
"warningLevel": 3,
"body": "test message"
},
"error": false,
"requestTime": 36,
"serverTime": "2026-03-06T12:52:47.376Z"
}Get all
curl http://localhost:7788/messages
{
"result": [
{
"id": 1,
"timeCreated": "2026-03-06T12:52:43.568Z",
"timeUpdated": "2026-03-06T12:52:47.350Z",
"warningLevel": 3,
"body": "test message"
}
],
"meta": {
"total": 1,
"limit": 0,
"skip": 0,
"page": 1,
"pages": 1,
"isFirstPage": true,
"isLastPage": true
},
"error": false,
"requestTime": 6,
"serverTime": "2026-03-06T12:52:54.800Z"
}Get one
curl http://localhost:7788/messages/1
{
"result": {
"id": 1,
"timeCreated": "2026-03-06T12:52:43.568Z",
"timeUpdated": "2026-03-06T12:52:47.350Z",
"warningLevel": 3,
"body": "test message"
},
"error": false,
"requestTime": 16,
"serverTime": "2026-03-06T12:53:03.442Z"
}Delete
curl -X DELETE http://localhost:7788/messages/1
{
"result": {
"ok": true
},
"meta": {
"countDeleted": 1
},
"error": false,
"requestTime": 12,
"serverTime": "2026-03-06T12:53:16.420Z"
}Query parameters for GET all
| Parameter | Description | Example |
| --- | --- | --- |
| _sort | Sort by fields (-field for DESC, random() allowed) | ?_sort=-timeCreated,warningLevel |
| _limit | Limit number of records | ?_limit=20 |
| _page | Page number (1-based) | ?_page=2&_limit=20 |
| _skip | Skip records | ?_skip=10&_limit=20 |
| _unlimited | Return all records (requires CAN_GET_UNLIMITED=true) | ?_unlimited=true |
| _after | Cursor pagination (works with _sort + _limit) | ?_sort=-timeCreated&_limit=20&_after=2026-03-06T12:52:43.568Z |
| _fields | Return only selected fields | ?_fields=id,warningLevel,body |
| _join | Include joinOnDemand relations | ?_join=author,comments |
| _search | Full-text/trigram search (when searchFields configured) | ?_search=test |
| _lang | Translate fields via dict table | ?_lang=de |
| <field> | Exact match (IN if repeated or array) | ?warningLevel=3 |
| <field>~ | Case-insensitive LIKE (ilike) for string fields | ?body~=%test% |
| <field>! | Not equal (NOT IN for array/repeated values) | ?warningLevel!=1 |
| _null_<field> | IS NULL filter (for nullable columns) | ?_null_timeUpdated=1 |
| _not_null_<field> | IS NOT NULL filter (for nullable columns) | ?_not_null_timeUpdated=1 |
| _from_<field> | Range lower bound (>=) for non-boolean columns | ?_from_warningLevel=2 |
| _to_<field> | Range upper bound (<=) for non-boolean columns | ?_to_warningLevel=3 |
| _in_<field> | IN from JSON array for non-boolean columns | ?_in_id=[1,2,3] |
| _not_in_<field> | NOT IN from JSON array for non-boolean columns | ?_not_in_id=[4,5] |
Overriding methods
You can override any CRUD method with custom logic. Just add a new route or a route with the same path and method before router.crud(...):
import { Routings } from 'the-api';
const router = new Routings({ migrationDirs: ['./migrations'] });
router.crud({ table: 'messages' });
const routerOverride = new Routings();
routerOverride.get('/messages', async (c, next) => {
c.var.appendQueryParams({ userId: c.var.user.userId });
await next();
});
export default [routerOverride, router];Query params helper
If you only need to update request query params before the next handler, use c.var.appendQueryParams(...):
import { Routings } from 'the-api';
const router = new Routings();
router.get('/messages', async (c, next) => {
c.var.appendQueryParams({
userId: c.var.user.userId,
modified: true,
});
await next();
});appendQueryParams merges new params into the current URL query, updates c.req.raw, and refreshes c.var.query.
Existing keys are overwritten, arrays are written as repeated query params, and null / undefined remove the param.
Request state helpers
Each request now gets normalized request data in c.var:
c.var.query: normalized query object. Single values stay strings, repeated params become arrays.c.var.body: request body parsed exactly once byContent-Type.c.var.bodyType: one ofempty,json,form,text,arrayBuffer.
c.var.body rules:
application/json-> parsed JSONmultipart/form-data/application/x-www-form-urlencoded-> object built fromformData()text/*-> string- any other non-empty body ->
ArrayBuffer
Form bodies keep strings and File values. Repeated fields become arrays, and fields ending with [] are also stored as arrays.
If body parsing fails, the-api logs [body parse error] and continues:
json/form->{}text->''arrayBuffer-> emptyArrayBuffer
So malformed body does not automatically throw in the global request parser. If you replace c.req or c.req.raw manually inside a handler, also update c.var.body, c.var.bodyType, and/or c.var.query yourself.
Validation example
router.crud({ table: 'messages' }) auto-builds validation from DB schema:
params.idtype is inferred from primary key type.query._sortaccepts only table fields.body.post/body.patchare inferred from table columns.required: trueforPOSTis inferred fromNOT NULLcolumns without default.- simple
CHECKconstraints (>,>=,<,<=,IN) are mapped tomin,max,enum.
router.crud({ table: 'messages' });Custom validation can override only part of schema:
router.crud({
table: 'messages',
validation: {
body: {
post: {
warningLevel: { type: 'number', min: 1, max: 3 },
body: { type: 'string', required: true },
},
},
},
});Disable all validation or only selected sections:
router.crud({ table: 'messages', validation: {} });
router.crud({
table: 'messages',
validation: { params: {} },
});body.post and body.patch can be functions:
router.crud({
table: 'messages',
validation: {
body: {
post: (c, next) => ({
warningLevel: { type: 'number', min: 0, max: 2 },
body: { type: 'string', required: true },
}),
patch: (c, next) => ({
validate: (value) => true,
}),
},
},
});Zod integration example: https://github.com/the-api/the-api-validators-zod
Field rules example
fieldRules is the only supported structure for field-level access configuration.
import Roles from 'the-api-roles';
import { Routings, TheAPI } from 'the-api';
const roles = new Roles({
root: ['*'],
admin: ['users.getFullInfo', 'users.editEmail'],
registered: ['users.getViews'],
owner: ['users.getFullInfo'], // virtual owner permissions
});
const router = new Routings();
router.crud({
table: 'users',
fieldRules: {
hidden: ['password', 'salt'], // always hidden
readOnly: ['emailVerified'], // not writable via POST/PATCH
visibleFor: {
'users.getFullInfo': ['email', 'phone', 'timeCreated'],
'users.getViews': ['views'],
},
editableFor: {
'users.editEmail': ['email'],
},
},
});
const theAPI = new TheAPI({ roles, routings: [router] });How fieldRules works:
hidden: fields are removed from response by default.readOnly: fields are filtered out from incomingPOST/PATCHpayloads.hiddenfields are always added to read-only set.visibleFor: permission -> fields mapping for showing hidden fields.- owner visibility is supported: if the record belongs to current user (
userIdby default), owner permissions are used for owner-view rules.
Notes:
- field-level behavior is configured only via
fieldRules. - If
readOnlyis not provided, default readonly fields are used:id,timeCreated,timeUpdated,timeDeleted,isDeleted.
Roles & Permissions example
the-api-roles is a role and permission management library for the-api. It is used to describe available roles, resolve inherited permissions, and check access in routes and CRUD handlers.
import Roles from 'the-api-roles';
import { Routings, TheAPI } from 'the-api';
const roles = new Roles({
root: ['*'], // all permissions
admin: [
'_.registered', // nested permissions: all permissions of registered role
'users.getFullInfo', // get full info of users
'users.editEmail', // edit email
'testNews.*' // all permissions for testNews
],
registered: ['testNews.get', 'users.get'], // only get permissions for testNews and users
unverified: ['_.guest', 'users.getPublicInfo'], // limited permissions for unverified users
guest: ['users.login'], // permissions for guests (unauthenticated users)
owner: ['users.getFullInfo', 'users.editEmail'], // virtual role, resolved per record
});
const router = new Routings({ migrationDirs: ['./tests/migrations'] });
router.get('/users', async (c, next) => {
await roles.checkPermission('users.get');
await next();
});
router.crud({
table: 'testNews',
permissions: {
methods: ['POST', 'PATCH', 'DELETE'], // explicit protection for selected CRUD methods
owner: ['testNews.getFullInfo'], // explicit owner permissions for this CRUD
},
});
const theAPI = new TheAPI({ roles, routings: [router] });How permissions works:
methods: enables route-level role check for selected CRUD methods.methodsaccepts['GET', 'POST', 'PATCH', 'DELETE']or['*'](uppercase).- Mapping to routes:
GETprotects both/<prefix|table>and/<prefix|table>/:idPOSTprotects only/<prefix|table>PATCHandDELETEprotect/<prefix|table>/:id
- If
permissions.methodsis not provided, methods are inferred automatically from role permissions:- the system scans all role values and finds
<prefix|table>.<method>and<prefix|table>.* - example: role permission
users.delete-> inferredmethods: ['DELETE'] - list of supported methods for inference:
get,post,patch,delete(case-insensitive) - if no matching permissions exist, method-level protection is not added
- the system scans all role values and finds
- If
permissions.methodsis provided, it overrides auto-inference. permissions.owner: owner-specific permissions for field visibility rules.- If omitted, and roles service exists, owner permissions are resolved from role name
owner.
- If omitted, and roles service exists, owner permissions are resolved from role name
- field visibility/editability rules are configured in
fieldRules(visibleFor,editableFor).
Error example
curl http://localhost:7788/d{
"result": {
"error": true,
"code": 21,
"status": 404,
"description": "Not found",
"name": "NOT_FOUND",
"additional": []
},
"error": true,
"requestTime": 2,
"serverTime": "2026-03-05T13:48:42.543Z"
}.env
PORT=3000 (default 7788)
Response structure
example:
{
result: {},
relations: {},
meta: {},
error: false,
requestTime: 2,
serverTime: "2024-05-18T10:39:49.795Z",
logId: "3n23rp20",
}Fields Description
result: API response result, set withc.set('result', ...).relations: Related objects associated with the main object.meta: API response metadata (e.g., page number, total pages), set withc.set('meta', ...).error: Error flag (true/false) indicating if there was an error.requestTime: Time spent on the server to process the request, in milliseconds.serverTime: Current server time.logId: Request's log ID (used inlogsmiddleware).
Middlewares
common
middlewares.common is a ready-made group of base middlewares:
middlewares.logsmiddlewares.errorsmiddlewares.status
It is useful when you usually want logging, error handling, and GET /status together.
import { Routings, TheAPI, middlewares } from 'the-api';
const router = new Routings();
const theAPI = new TheAPI({
routings: [middlewares.common, router],
});logs
logs middleware logs all requests and responses with unique request id, method, path, time on server, log information
data and time, unique request id, method, path, time on server, log information
each request starts with [begin] and ends with [end]
after begin you can see information about request
The following keys will mark as hidden: 'password', 'token', 'refresh', 'authorization'
you can use c.var.log() to add any info to logs
import { Routings, TheAPI, middlewares } from 'the-api';
const router = new Routings();
router.get('/data/:id', async (c) => {
c.var.log(c.req.param());
c.set('result', { foo: 'bar' });
});
const theAPI = new TheAPI({
routings: [
middlewares.logs, // logs middleware
router,
]
});
await theAPI.up();curl http://localhost:7788/data/123Output:
[2026-03-05T15:33:14.727Z] [j9szsmhn] [GET] [/data/123] [1] [begin]
[2026-03-05T15:33:14.727Z] [j9szsmhn] [GET] [/data/123] [1] {"headers":{"host":"localhost:7788","user-agent":"curl/7.68.0","accept":"*/*"},"query":{},"body":"","method":"GET","path":"/data/123"}
[2026-03-05T15:33:14.728Z] [j9szsmhn] [GET] [/data/123] [2] params: [object Object]
[2026-03-05T15:33:14.728Z] [j9szsmhn] [GET] [/data/123] [2] {"foo":"bar"}
[2026-03-05T15:33:14.728Z] [j9szsmhn] [GET] [/data/123] [2] [end]cors
cors is re-exported from hono/cors.
Docs: https://hono.dev/docs/middleware/builtin/cors
If you need to enable CORS for all methods on all routes, use *:
import { Routings, TheAPI, cors } from 'the-api';
const router = new Routings();
router.get('/data', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', cors());
await theAPI.up();csrf
csrf is re-exported from hono/csrf.
Docs: https://hono.dev/docs/middleware/builtin/csrf
If you need CSRF protection for all methods on all routes, use *:
import { Routings, TheAPI, csrf } from 'the-api';
const router = new Routings();
router.post('/posts', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', csrf());
await theAPI.up();If you need to allow requests from a specific origin:
import { Routings, TheAPI, csrf } from 'the-api';
const router = new Routings();
router.post('/posts', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', csrf({ origin: 'https://app.example.com' }));
await theAPI.up();body-limit
bodyLimit is based on hono/body-limit.
Docs: https://hono.dev/docs/middleware/builtin/body-limit
If you need to limit request body size for all methods on all routes, use *:
import { Routings, TheAPI, bodyLimit } from 'the-api';
const router = new Routings();
router.post('/upload', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', bodyLimit({ maxSize: 1024 * 1024 }));
await theAPI.up();compress
compress is re-exported from hono/compress.
Docs: https://hono.dev/docs/middleware/builtin/compress
If you need response compression for all methods on all routes, use *:
import { Routings, TheAPI, compress } from 'the-api';
const router = new Routings();
router.get('/data', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', compress());
await theAPI.up();etag
etag is re-exported from hono/etag.
Docs: https://hono.dev/docs/middleware/builtin/etag
If you need ETag headers for all methods on all routes, use *:
import { Routings, TheAPI, etag } from 'the-api';
const router = new Routings();
router.get('/data', async (c) => {
c.set('result', { ok: true });
});
const theAPI = new TheAPI({
routings: [router],
});
theAPI.app.use('*', etag());
await theAPI.up();errors
Every exception generates error response with error flag set to true
Also, error response contains code, status, main message, stack and additional.
additional is always an array of objects with at least message.
import { Routings, TheAPI, middlewares } from 'the-api';
const router = new Routings();
router.errors({ // set user-defined errors
USER_DEFINED_ERROR: {
code: 55,
status: 403,
description: 'user defined error description',
},
});
router.get('/error', async (c) => {
throw new Error('USER_DEFINED_ERROR'); // throw user-defined error
});
router.get('/additional', async (c) => { // user-defined with additional information
throw new Error('USER_DEFINED_ERROR: additional information');
});
const theAPI = new TheAPI({
routings: [
middlewares.errors, // errors middleware
router,
]
});
await theAPI.up();User-defined error
curl http://localhost:7788/errorOutput:
{
"result": {
"code": 55,
"status": 403,
"description": "user defined error description",
"name": "USER_DEFINED_ERROR",
"additional": [],
"stack": "Error: USER_DEFINED_ERROR\n at ...stack trace...",
"error": true
},
"error": true,
"serverTime": "2026-03-05T15:48:28.369Z"
}User-defined error with additional information
curl http://localhost:7788/additionalOutput:
{
"result": {
"code": 55,
"status": 403,
"description": "user defined error description",
"name": "USER_DEFINED_ERROR",
"additional": [{ "message": "additional information" }],
"stack": "Error: USER_DEFINED_ERROR\n at ...stack trace...",
"error": true
},
"error": true,
"serverTime": "2026-03-05T15:48:28.369Z"
}Error with meta information
router.get('/meta', async (c: any) => {
c.set('meta', { x: 3 });
throw new Error('error message');
});{
result: {
code: 11,
status: 500,
description: "An unexpected error occurred",
message: "error message",
additional: [],
stack: "...stack...",
error: true,
},
meta: {
x: 3,
},
error: true,
requestTime: 1,
serverTime: "2024-05-18T08:17:56.929Z",
logId: "06zqxkyb",
}404 Not Found
curl http://localhost:7788/not-found{
result: {
code: 21,
status: 404,
description: "Not found",
message: "NOT_FOUND",
additional: [],
error: true,
},
error: true,
requestTime: 0,
serverTime: "2024-05-18T16:56:21.501Z",
}500 Internal Server Error
{
result: {
code: 11,
status: 500,
description: "An unexpected error occurred",
message: "error message",
additional: [],
stack: "...stack...",
error: true,
},
error: true,
requestTime: 1,
serverTime: "2024-05-18T08:17:56.929Z",
logId: "06zqxkyb",
}Status middleware
GET /status
{
result: {
ok: 1,
},
error: false,
requestTime: 1,
serverTime: "2024-05-18T08:17:56.929Z",
logId: "06zqxkyb",
}Routes
All like in Hono Routing, but you can set response result and response metadata the following way:
c.set('result', ...)
c.set('meta', ...)
Using Routings
Routings in the-api is a re-export from the-api-routings.
So all CRUD behavior (router.crud(...)) is implemented in the-api-routings.
import { Routings, TheAPI } from 'the-api';
const router = new Routings();
// your routing rules here
const theAPI = new TheAPI({ routings: [router] });
export default theAPI.up();routings supports nested arrays too, so you can group reusable middleware sets:
import { Routings, TheAPI, middlewares } from 'the-api';
const route1 = new Routings();
const route2 = new Routings();
const theAPI = new TheAPI({
routings: [route1, [middlewares.common, route2]],
});Prefix route groups
Use prefix() when you want to register several routes under the same base path:
const router = new Routings();
router
.prefix('/ships')
.get('/:id/similar', getSimilarShips)
.get('/:id/requests', getRequests)
.post('/import', importShip)
.post('/0', parseShip)
.post('/0/countries', guessCountryByName);This is equivalent to:
router.get('/ships/:id/similar', getSimilarShips);
router.get('/ships/:id/requests', getRequests);
router.post('/ships/import', importShip);
router.post('/ships/0', parseShip);
router.post('/ships/0/countries', guessCountryByName);Calling prefix() again switches the current base path:
router
.prefix('/v1')
.get('/users', getUsersV1)
.prefix('/v2')
.get('/users', getUsersV2);The current prefix also applies to router.crud(...):
router.prefix('/api/v1').crud({ table: 'messages' });
// GET /api/v1/messages
// POST /api/v1/messages
// GET /api/v1/messages/:id
// PATCH /api/v1/messages/:id
// DELETE /api/v1/messages/:idGet route
const router = new Routings();
router.get('data/:id', async (c: Context, n: Next) => {
await n();
c.set('result', {...c.var.result, e11: 'Hi11'});
});
router.get('data/:id', async (c: Context) => {
c.set('result', {e22: 'Hi22', ...c.req.param()});
});
const theAPI = new TheAPI({ routings: [router] });
export default theAPI.up();GET /data/12
{
result: {
e22: "Hi22",
id: "12",
e11: "Hi11",
},
requestTime: 2,
serverTime: "2024-05-18T14:07:12.459Z",
}Post route
router.post('/post', async (c: Context) => { const body = c.var.body as Record<string, unknown>; c.set('result', body); });
Patch route
router.patch('/patch/:id', async (c: Context) => { const body = c.var.body as Record<string, unknown>; c.set('result', {...c.req.param(), ...body}); });
Delete route
router.delete('/patch/:id', async (c: Context) => { const body = c.var.body as Record<string, unknown>; c.set('result', body); });
Files
middlewares.files injects c.var.files with an instance of Files.
If you need to override storage options in code, use middlewares.createFiles(...).
Local storage uses FILES_FOLDER. MinIO storage uses the existing MINIO_* variables.
Basic upload keeps the original file name:
import { Routings, TheAPI, middlewares } from 'the-api';
const router = new Routings();
router.post('/upload', async (c) => {
const body = c.var.body as Record<string, unknown>;
const result = await c.var.files.upload(body.file as File, 'uploads');
c.set('result', result);
});
const theAPI = new TheAPI({
routings: [middlewares.files, router],
});
await theAPI.up();Or with an explicit folder:
const theAPI = new TheAPI({
routings: [
middlewares.createFiles({ folder: 'public' }),
router,
],
});If IMAGE_SIZES is set and the uploaded file is an image, Files.upload(...) creates resized webp variants automatically.
Environment variable format:
IMAGE_SIZES=small:200x150,medium:600x400,large:1200x900
IMAGE_NAME_LENGTH_BYTES=6For images, upload(file, 'uploads') generates a random hex name and stores files in nested folders using the first four characters.
IMAGE_NAME_LENGTH_BYTES controls the randomBytes(...) size for that name; the default is 6, which produces a 12-character hex name such as abcdef123456.
/path_to_save/ab/cd/abcdef123456/small.webp
/path_to_save/ab/cd/abcdef123456/medium.webp
/path_to_save/ab/cd/abcdef123456/large.webpsmall, medium, large are taken from IMAGE_SIZES. Non-image files are still stored without resizing.
Helpful workflow methods on c.var.files:
getBodyFiles(body, { fields, imagesOnly })uploadMany(files, destDir, { imagesOnly })uploadBody(body, destDir, { fields, imagesOnly })getImageSizes()getImageDir(destDir, imageName)getImageVariantPath(destDir, imageName, sizeName)deleteImage(imageName, destDir)
Example for image-only upload from multipart body:
router.post('/upload-images', async (c) => {
const body = c.var.body as Record<string, unknown>;
const files = c.var.files.getBodyFiles(body, {
fields: ['file', 'file[]'],
imagesOnly: true,
});
const result = await c.var.files.uploadMany(files, 'images');
c.set('result', result);
});Or shorter:
router.post('/upload-images', async (c) => {
const body = c.var.body as Record<string, unknown>;
const result = await c.var.files.uploadBody(body, 'images', {
fields: ['file', 'file[]'],
imagesOnly: true,
});
c.set('result', result);
});Example result for an image upload:
{
"path": "/path_to_save/ab/cd/abcdef123456",
"name": "abcdef123456",
"originalName": "photo.png",
"size": 153245,
"sizes": {
"small": {
"path": "/path_to_save/ab/cd/abcdef123456/small.webp",
"width": 200,
"height": 150,
"size": 842
},
"medium": {
"path": "/path_to_save/ab/cd/abcdef123456/medium.webp",
"width": 600,
"height": 400,
"size": 2948
},
"large": {
"path": "/path_to_save/ab/cd/abcdef123456/large.webp",
"width": 1200,
"height": 900,
"size": 10432
}
}
}IMAGE_SIZES must use the name:WIDTHxHEIGHT format.
IMAGE_NAME_LENGTH_BYTES must be a positive integer.
TestClient For Integration Tests
testClient lets you spin up TheAPI for tests and gives you convenient request helpers (get/post/patch/delete), generated JWT tokens, and utilities like deleteTables/truncateTables.
Import:
import { createRoutings, testClient } from 'the-api';Parameters of testClient(options?):
migrationDirs?: string[]- migration folders used by internal routings created insidetestClientcrudParams?: CrudBuilderOptionsType[]- list ofrouter.crud(...)configs that will be registered automaticallyroles?: Roles | Record<string, string[]>- roles instance or roles map (map is converted tonew Roles(...)internally)routings?: RoutingsInputType- extra routings/middlewares you already created; nested arrays are supportednewRoutings?: (router: Routings) => void- callback to append custom routes to a new internalRoutingsinstancetheApiOptions?: Omit<TheApiOptionsType, 'routings' | 'roles' | 'migrationDirs'>- additionalTheAPIoptions (for exampleport,emailTemplates)
Helper:
createRoutings(options?)- shorthand fornew Routings(options), useful in tests for better readability and typed setup.
What testClient returns:
theAPI- already initializedTheAPIinstance, useful when you need direct access totheAPI.appclient- request helper with methods:get,post,patch,delete,postForm,postFormRequest,deleteTables,truncateTables,readFile,generateGWT,storeValue,getValuetokens- ready-to-use JWT tokens for default test users (root,admin,registered,manager,unknown,noRole) andnoTokenas an empty stringusers- default test users payload (root,admin,registered,manager,unknown,noRole)DateTime-luxonDateTimeexport for convenience in tests
Minimal example:
import { test, expect } from 'bun:test';
import { testClient } from 'the-api';
const { client } = await testClient({
crudParams: [{ table: 'messages' }],
migrationDirs: ['./migrations'],
});
test('messages list', async () => {
const { result } = await client.get('/messages');
expect(Array.isArray(result)).toEqual(true);
});Example with all parameters and returned helpers:
import { test, expect } from 'bun:test';
import { Routings, middlewares, testClient } from 'the-api';
const router = new Routings({ migrationDirs: ['./migrations'] });
router.get('/ping', async (c) => c.set('result', { ok: true }));
const roles = {
root: ['*'],
admin: ['messages.get'],
};
const {
theAPI,
client,
tokens,
users,
DateTime,
} = await testClient({
migrationDirs: ['./migrations'],
crudParams: [{ table: 'messages' }],
roles,
routings: [middlewares.errors, router],
theApiOptions: { port: 7788, emailTemplates: { demo: { subject: 's', text: 't' } } },
});
test('full testClient usage', async () => {
await client.post('/messages', { body: `created ${DateTime.now().toISO()}` }, tokens.root);
const { result } = await client.get('/messages', tokens.admin);
expect(result.length > 0).toEqual(true);
expect(users.root.userId).toEqual(1);
await client.truncateTables('messages');
});Example with createRoutings + routings:
import { test, expect } from 'bun:test';
import { createRoutings, testClient } from 'the-api';
import type { AppContext } from 'the-api';
const router = createRoutings({ migrationDirs: ['./tests/migrations'] });
router.get('/check-migration', async (c: AppContext) => {
await c.var.dbWrite('testNews').insert({ name: 'test' });
c.set('result', await c.var.db('testNews'));
});
const { client } = await testClient({ routings: [router] });
test('migration route', async () => {
const { result } = await client.get('/check-migration');
expect(result[0].name).toEqual('test');
});Example with newRoutings (no manual router creation):
import { test, expect } from 'bun:test';
import { testClient } from 'the-api';
import type { AppContext, TestClientOptionsType } from 'the-api';
const newRoutings: NonNullable<TestClientOptionsType['newRoutings']> = (router) => {
router.get('/check-migration', async (c: AppContext) => {
await c.var.dbWrite('testNews').insert({ name: 'test' });
c.set('result', await c.var.db('testNews'));
});
};
const { client } = await testClient({
migrationDirs: ['./tests/migrations'],
newRoutings,
});
test('migration route via newRoutings', async () => {
const { result } = await client.get('/check-migration');
expect(result[0].name).toEqual('test');
});