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

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-api installed.
  • Then, create .env, ./migrations/20260305134700_create_messages_table.ts and ./index.ts files 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 your package.json and start your project with node --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 by Content-Type.
  • c.var.bodyType: one of empty, json, form, text, arrayBuffer.

c.var.body rules:

  • application/json -> parsed JSON
  • multipart/form-data / application/x-www-form-urlencoded -> object built from formData()
  • 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 -> empty ArrayBuffer

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.id type is inferred from primary key type.
  • query._sort accepts only table fields.
  • body.post/body.patch are inferred from table columns.
  • required: true for POST is inferred from NOT NULL columns without default.
  • simple CHECK constraints (>, >=, <, <=, IN) are mapped to min, 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 incoming POST/PATCH payloads.
  • hidden fields 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 (userId by default), owner permissions are used for owner-view rules.

Notes:

  • field-level behavior is configured only via fieldRules.
  • If readOnly is 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.
  • methods accepts ['GET', 'POST', 'PATCH', 'DELETE'] or ['*'] (uppercase).
  • Mapping to routes:
    • GET protects both /<prefix|table> and /<prefix|table>/:id
    • POST protects only /<prefix|table>
    • PATCH and DELETE protect /<prefix|table>/:id
  • If permissions.methods is 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 -> inferred methods: ['DELETE']
    • list of supported methods for inference: get, post, patch, delete (case-insensitive)
    • if no matching permissions exist, method-level protection is not added
  • If permissions.methods is 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.
  • 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 with c.set('result', ...).
  • relations: Related objects associated with the main object.
  • meta: API response metadata (e.g., page number, total pages), set with c.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 in logs middleware).

Middlewares

common

middlewares.common is a ready-made group of base middlewares:

  • middlewares.logs
  • middlewares.errors
  • middlewares.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/123

Output:

[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/error

Output:

{
    "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/additional

Output:

{
    "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/:id

Get 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=6

For 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.webp

small, 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 inside testClient
  • crudParams?: CrudBuilderOptionsType[] - list of router.crud(...) configs that will be registered automatically
  • roles?: Roles | Record<string, string[]> - roles instance or roles map (map is converted to new Roles(...) internally)
  • routings?: RoutingsInputType - extra routings/middlewares you already created; nested arrays are supported
  • newRoutings?: (router: Routings) => void - callback to append custom routes to a new internal Routings instance
  • theApiOptions?: Omit<TheApiOptionsType, 'routings' | 'roles' | 'migrationDirs'> - additional TheAPI options (for example port, emailTemplates)

Helper:

  • createRoutings(options?) - shorthand for new Routings(options), useful in tests for better readability and typed setup.

What testClient returns:

  • theAPI - already initialized TheAPI instance, useful when you need direct access to theAPI.app
  • client - request helper with methods: get, post, patch, delete, postForm, postFormRequest, deleteTables, truncateTables, readFile, generateGWT, storeValue, getValue
  • tokens - ready-to-use JWT tokens for default test users (root, admin, registered, manager, unknown, noRole) and noToken as an empty string
  • users - default test users payload (root, admin, registered, manager, unknown, noRole)
  • DateTime - luxon DateTime export 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');
});