Quickstart

Use Node 24 or newer. Development verification runs on Node 24. This example uses an in-memory SQLite database: restarting the process discards its records.

Install

mkdir quickstart-api
cd quickstart-api
npm init -y
npm pkg set type=module
npm install json-rest-api knex better-sqlite3 express

These examples use the revised API described in the migration guide. Until that release is published, install the intended library tarball in place of the registry package.

Define resources

Create index.js with the following code. Install the HTTP connector before registering resources so it can register their routes.

import { JsonRestApi, RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api'
import knexLib from 'knex'
import express from 'express'

const knex = knexLib({
  client: 'better-sqlite3',
  connection: { filename: ':memory:' },
  useNullAsDefault: true
})
const api = new JsonRestApi({ name: 'book-catalog-api' })
await api.use(RestApiPlugin)
await api.use(RestApiKnexPlugin, { knex })
await api.use(ExpressPlugin, { mountPath: '/api' })

await api.addResource('publishers', {
  schema: {
    name: { type: 'string', required: true, max: 255, search: true }
  },
  relationships: {
    authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
  }
})
await api.addResource('authors', {
  schema: {
    name: { type: 'string', required: true, max: 100, search: true },
    surname: { type: 'string', required: true, max: 100 },
    publisher_id: { type: 'id', belongsTo: 'publishers', as: 'publisher', nullable: true }
  },
  searchSchema: {
    nameContains: { type: 'string', actualField: 'name', filterOperator: 'contains' }
  }
})
await api.resources.publishers.createKnexTable()
await api.resources.authors.createKnexTable()

searchSchema declares accepted public filters; actualField maps a filter to a resource field. See searching for more complex filters and schema migrations for persistent databases.

Call the API programmatically

Append this code before starting the HTTP server:

const publisher = await api.resources.publishers.post({
  data: { name: 'Penguin Random House' }
})
const author = await api.resources.authors.post({
  data: { name: 'George', surname: 'Orwell', publisher: publisher.id }
})

const found = await api.resources.authors.query({
  queryParams: {
    filters: { nameContains: 'Georg' },
    sort: ['surname'],
    page: { number: 1, size: 10 }
  }
})
console.log('Matching authors:', found.data)
console.log('Pagination:', found.meta.pagination)

const withAuthors = await api.resources.publishers.get({
  id: publisher.id,
  queryParams: { include: ['authors'] }
})
console.log('Publisher and included authors:', withAuthors)

const updated = await api.resources.authors.patch({
  id: author.id,
  data: { surname: 'Orwell (Eric Blair)' },
  returning: 'minimal'
})
console.log('Updated identifier:', updated)

const document = await api.resources.publishers.post({
  format: 'jsonapi',
  document: {
    data: {
      type: 'publishers',
      attributes: { name: 'Oxford University Press' }
    }
  }
})
console.log('JSON:API publisher:', document.data)

Programmatic calls default to format: 'plain' and returning: 'full'. Writes accept either plain resource values in data or a JSON:API document in document, never both. Read selection, filtering, sorting and pagination go in queryParams; identifiers and response options remain method parameters.

Plain reads return a record for get, and a collection with data for query. The filtered query above returns George and pagination metadata with total: 1. The PATCH returns { type: 'authors', id: author.id } because it selects returning: 'minimal'. Use returning: 'none' for an undefined write result. format selects only the output. You can request JSON:API output from plain data, or plain output from a JSON:API document. The input key selects the input contract; the library does not guess from its contents.

Start the HTTP server

Append this last block:

const app = express()
app.use(api.http.express.router)
app.use(api.http.express.notFoundRouter)
const server = app.listen(3000, () => {
  console.log('API listening at http://localhost:3000/api')
})

async function shutdown () {
  server.close(async () => { await knex.destroy() })
}
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)

Run node index.js. HTTP requests use JSON:API documents. The connector handles request-body parsing. The following commands assume a fresh run of the example; use returned IDs when adapting them to an existing database. --globoff keeps curl from interpreting the square brackets in query parameters.

curl http://localhost:3000/api/publishers
curl --globoff 'http://localhost:3000/api/publishers/1?include=authors'
curl --globoff 'http://localhost:3000/api/authors?filter[nameContains]=Georg'
curl --globoff 'http://localhost:3000/api/authors?fields[authors]=name,surname'
curl --globoff 'http://localhost:3000/api/publishers?page[number]=1&page[size]=10'
curl 'http://localhost:3000/api/authors?sort=-surname'

curl -X POST http://localhost:3000/api/authors \
  -H 'Content-Type: application/vnd.api+json' \
  -d '{"data":{"type":"authors","attributes":{"name":"Jane","surname":"Austen"},"relationships":{"publisher":{"data":{"type":"publishers","id":"1"}}}}}'

curl -X PATCH http://localhost:3000/api/authors/2 \
  -H 'Content-Type: application/vnd.api+json' \
  -d '{"data":{"type":"authors","id":"2","attributes":{"surname":"Austen (1775-1817)"}}}'

curl -X DELETE http://localhost:3000/api/authors/2

For an API without an HTTP server, omit the connector/server setup and call await knex.destroy() after the programmatic operations finish.

Continue with the full guide, API reference, and verified backend capabilities.