Fastify Resource
A way of creating API routes in Fastify with Objection.js models.

Dependencies
- Node.js
- Fastify
- Objection.js for models
Install
npm i @anephenix/fastify-resource
Why use fastify-resource?
When writing code for an API, you may find yourself generating RESTful routes
for Objection.js models that support CRUD operations (Create/Read/Update/
Delete), and it could end up looking like this:
import fastify from 'fastify';
import Person from './models/Person';
const app = fastify({ logger: false });
app.get('/people', async (req, rep) => {
try {
const data = await Person.query();
return data;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
app.post('/people', async (req, rep) => {
try {
const people = await Person.query().insert(req.body);
rep.statusCode(201);
return people;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
app.get('/people/:id', async (req, rep) => {
try {
const person = await Person.query().findById(req.params.id);
if (person) return person;
if (!person) {
res.statusCode(404);
return 'Not found';
}
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
app.patch('/people/:id', async (req, rep) => {
try {
const person = await Person.query().patchAndFetchById(
req.params.id,
req.body
);
if (person) return person;
if (!person) {
res.statusCode(404);
return 'Not found';
}
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
app.delete('/people/:id', async (req, rep) => {
try {
await Person.query().deleteById(req.params.id);
return req.params.id;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
To save you from having to write all that code, this library works as a
fastify plugin to enable you to do the same thing, but with just these
lines of code:
import fastify from 'fastify'
import Person from './models/Person';
import fastifyResource from '@anephenix/fastify-resource';
const app = fastify({ logger: false });
app.register(fastifyResource, {
model: Person,
resourceList: 'person',
});
This will automatically generate and register the following RESTful routes:
GET /people
POST /people
GET /people/:id
PATCH /people/:id
DELETE /people/:id
It will also:
- Automatically generate code functions for each of those endpoints in memory
- Those code function then call the Objection.model to perform database operations.
The result being that in a few lines of code you have implemented CRUD for
your resource.
Adding a preHandler
If you want to run Fastify pre-handlers before each generated route (for example to apply auth or add headers), pass a preHandler function or array in the plugin options:
const ensureAuth = async (request, reply) => {
if (!request.headers.authorization) {
reply.code(401);
throw new Error("Unauthorized");
}
};
app.register(fastifyResource, {
model: Person,
resourceList: 'person',
preHandler: ensureAuth,
});
You can also supply an array of pre-handlers:
app.register(fastifyResource, {
model: Person,
resourceList: 'person',
preHandler: [ensureAuth, otherHook],
});
Creating nested routes
Any REST API tends to implement a hierarchy of resources. Let's say for example
there are 2 models - Post and Comment. A post has many comments, and we want to
create an API that models that relationship (fetch comments for a post).
We might want our comments API routes to be nested under the posts API routes.
The library can do that:
import fastify from 'fastify'
import Post from './models/Post';
import fastifyResource from '@anephenix/fastify-resource';
const app = fastify({ logger: false });
app.register(fastifyResource, {
model: Post,
resourceList: ['post', 'comment'],
});
This will make the following API routes available on the fastify instance:
GET /posts
POST /posts
GET /posts/:id
PATCH /posts/:id
DELETE /posts/:id
GET /posts/:post_id/comments
POST /posts/:post_id/comments
GET /posts/:post_id/comments/:id
PATCH /posts/:post_id/comments/:id
DELETE /posts/:post_id/comments/:id
You can have many levels of nested resources in your code, it is not limited
to any number (we just showed 2 resources in order to demonstrate the example).
Support for self-referential resources
There might be a case where you use the same database table for a type of Model
that contains nested resources that are the same thing, such as:
- A Category model that has many sub-categories
- A Person model that has many children
- A Group that has many sub-groups
It is possible to setup the self-referential resource to perform queries using
the relationMappings part of the ORM model:
Lete's say that you have a model Person with a relationMappings for
children that looks like this:
import { Model } from "objection";
import { appDB } from "../../knexConnections";
import Possession from "./Possession";
Model.knex(appDB);
class Person extends Model {
firstName: unknown;
static get tableName() {
return "persons";
}
static get relationMappings() {
return {
children: {
relation: Model.HasManyRelation,
modelClass: Person,
join: {
from: "persons.id",
to: "persons.parentId",
},
}
};
}
}
export default Person;
And say you want to setup a REST API route set for these routes:
GET /people/:person_id/children
POST /people/:person_id/children
GET /people/:person_id/children/:id
PATCH /people/:person_id/children/:id
DELETE /people/:person_id/children/:id
Then you can achieve that by passing these properties in the serviceOptions
section:
app.register(fastifyResource, {
model: Person,
resourceList: ["person", "child"],
serviceOptions: {
type: "relatedQuery",
relatedQuery: "children",
primaryKey: "person_id",
},
});
Support for custom model actions
If you find that the ORM queries that the service generator uses do not match
your needs, and that you need to write custom model queries, then there is a
way to override it and to provide your own custom model action function to
the service generator.
You can generate a custom model action like this:
import type { Model } from 'objection';
const customModelAction = async (action: string, model: Model, params: Params) => {
const relatedQuery = 'children';
const primaryKey = 'person_id';
const primaryId = params[primaryKey];
const paramsToInsert = objectWithoutKey(params, primaryKey);
const paramsToUpdate = objectWithoutKey(
objectWithoutKey(paramsToInsert, "id"),
primaryKey,
);
switch (action) {
case "getAll":
return await model.relatedQuery(relatedQuery).for(primaryId);
case "get":
return await model
.relatedQuery(relatedQuery)
.for(primaryId)
.where("id", params.id)
.first();
case "create":
return await model
.relatedQuery(relatedQuery)
.for(primaryId)
.insert(paramsToInsert);
case "update":
return await model
.relatedQuery(relatedQuery)
.for(primaryId)
.patchAndFetchById(params.id, paramsToUpdate);
case "delete": {
const deletedCount = await model
.relatedQuery(relatedQuery)
.for(primaryId)
.delete()
.where("id", params.id);
if (deletedCount === 0) {
throw new Error(`Record with id ${params.id} not found`);
}
return params.id;
}
default:
throw new Error(`Unknown action: ${action}`);
}
};
app.register(fastifyResource, {
model: Person,
resourceList: ["person", "child"],
serviceOptions: {
customModelAction
},
});
The service will then use the customModelAction function when it comes to
performing the queries for the service.
Tests
npm t
License and Credits
Š2025 Anephenix Ltd. All Rights Reserved.