Fastify Resource
A way of creating RESTful CRUD resources for Fastify and Objection.js.
Dependencies
- Node.js
- Fastify
- Objection.js for models
Install
npm i @anephenix/fastify-resource
Usage
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).
This library provides a way to generate code that will provide that with a few
lines of code.
For example, in Fastify you will usually define routes and handler functions
for CRUD like this:
const fastify = require('fastify')({ logger: false });
const Asset = require('./models/Asset');
fastify.get('/assets', async (req, rep) => {
try {
const data = await Asset.query();
return data;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
fastify.post('/assets', async (req, rep) => {
try {
const assets = await Asset.query().insert(req.body);
rep.statusCode(201);
return assets;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
fastify.get('/assets/:id', async (req, rep) => {
try {
const asset = await Asset.query().findById(req.params.id);
if (asset) return asset;
if (!asset) {
res.statusCode(404);
return 'Not found';
}
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
fastify.patch('/assets/:id', async (req, rep) => {
try {
const asset = await Asset.query().patchAndFetchById(
req.params.id,
req.body
);
if (asset) return asset;
if (!asset) {
res.statusCode(404);
return 'Not found';
}
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
fastify.delete('/assets/:id', async (req, rep) => {
try {
await Asset.query().deleteById(req.params.id);
return req.params.id;
} catch (error) {
rep.statusCode(400);
return error.message;
}
});
There's about 60 lines of code there for the routes. Now if you have a lot of
resources that you want to generate a bunch of RESTful routes and CRUD actions
for, then this file will become hundreds of lines of code at the very least.
Also, you may find that you are repeating the same code but with different
Objection.js models and routes in place.
With fastify resource, you can write this code and it will do the same thing:
const fastify = require('fastify')({ logger: false });
const Asset = require('./models/Asset');
const { resource, attach } = require('@anephenix/fastify-resource');
const { routes } = resource(Asset, 'asset');
attach({ fastify, routes });
The resource
function is passed the Objection.js model as the 1st argument,
and the name of the resource for generating the url routes from as the 2nd
argument.
This will do the following:
- Define a list of 5 API RESTful routes that cover CRUD functions:
GET /assets
POST /assets
GET /assets/:id
PATCH /assets/:id
DELETE /assets/:id
- It will create the controller actions that support those API routes
- It will also create a service module that serves the controller
To then attach those routes to the fastify app, we call the attach
function
and pass the fastify instance and routes to combine them.
This helps you to quickly assemble a REST API for your Objection.js models,
using a few lines of code.
Route-Controller-Service pattern
The way that the library works is that it generates a set of objects that
provide functions that can be called:
Route -> Controller -> Service -> Model
Route
The first is the set of routes
, which when generated for a resource look
something like this:
[
{ method: 'get', url: '/assets', handler: Function },
{ method: 'post', url: '/assets', handler: Function },
{ method: 'get', url: '/assets/:id', handler: Function },
{ method: 'patch', url: '/assets/:id', handler: Function },
{ method: 'delete', url: '/assets/:id', handler: Function },
];
Controller
The handler
function is the controller action that will be called when
the API route is called. The controller is accessible from the resource
function call:
const { routes, controller, service } = resource(Asset, 'asset');
The generated controller code looks like this:
{
index: async (req:Request, rep:Reply) => {
const { success, data, error } = await service.getAll(req.params);
return handleResponse({success,data,error, rep});
},
create: async (req:Request, rep:Reply) => {
const params = Object.assign({}, req.params, req.body);
const { success, data, error } = await service.create(params);
return handleResponse({success,data,error, rep, successCode: 201});
},
get: async (req:Request, rep:Reply) => {
const { success, data, error } = await service.get(req.params);
return handleResponse({success,data,error, rep});
},
update: async (req:Request, rep:Reply) => {
const params = Object.assign({}, req.params, req.body);
const { success, data, error } = await service.update(params);
return handleResponse({success,data,error, rep});
},
delete: async (req:Request, rep:Reply) => {
const { success, data, error } = await service.delete(req.params);
return handleResponse({success,data,error, rep});
},
};
The controller
actions try to extract the relevant data from the HTTP
request and pass it to the relevant service action. It then handles the
response from the service, and returns the HTTP response suitable for it.
This makes the controller action essentially an interface to the service, and
the service is where the business logic for the action is defined.
Service
The service is the core part of the application's business logic, and is the
primary part of the code that interfaces with the Objection.js model. The
generated code for the service looks like this:
{
getAll: async (params:Params) => {
try {
const data = await model.query().where(params);
return { success: true, data };
} catch (error) {
return { success: false, error };
}
},
create: async (params:Params) => {
try {
const data = await model.query().insert(params);
return { success: true, data };
} catch (error) {
return { success: false, error };
}
},
get: async (params:Params) => {
try {
const data = await model.query().where(params);
return { success: true, data };
} catch (error) {
return { success: false, error };
}
},
update: async (params:Params) => {
try {
const updateParams = objectWithoutKey(params, 'id');
const data = await model
.query()
.patchAndFetchById(params.id, updateParams);
return { success: true, data };
} catch (error) {
return { success: false, error };
}
},
delete: async (params:Params) => {
try {
await model.query().deleteById(params.id);
return { success: true, data: params.id };
} catch (error) {
return { success: false, error };
}
},
};
Usually controller actions would be the primary code that interfaces with the
Objection.js model (like in the MVC pattern), but by abstracting out that part
into a service layer, you then have the opportunity to re-use those service
actions with other interfaces, such as:
- A WebSocket API
- An interactive REPL
- A CLI tool
We essentially treat the controller actions as supporting a HTTP API
interface to the service actions, alongside the other options.
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 fits that relationship. We might want our comments API
routes to be nested under the posts API routes.
The library can do that. In the resource
function, you can pass an array of
resources to assemble the API routes that fit that. Below is an example of
how to do that:
const fastify = require('fastify')({ logger: false });
const Post = require('./models/Post');
const Comment = require('./models/Comment');
const { resource, attach } = require('@anephenix/fastify-resource');
const postResource = resource(Post, 'post');
const commentResource = resource(Comment, ['post', 'comment']);
attach({ fastify, routes: postResource.routes });
attach({ fastify, routes: commentResource.routes });
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).
Advanced usage
There might be cases where you need to do custom adjustments to the controller,
or where you may want to access the service generated for the resource to attach
other interfaces to.
Generating routes, controllers and services separately
The library allows you to generate the services, controllers and routes
separately, so that you can structure your code in whatever pattern suits you.
The library has these functions available:
const {
serviceGenerator,
controllerGenerator,
resourceRoutes,
} = require('@anephenix/fastify-resource');
serviceGenerator
The serviceGenerator
function will generate a service for an Objection.js
model.
controllerGenerator
The controllerGenerator
function will generate a controller for a service.
resourceRoutes
The resourceRoutes
function will generate the routes for a controller and
the list of resources to turn into urls.
Using these will allow you to generate parts of the RCS pattern as well as
make custom parts of that code if needed.
Tests
npm t
License and Credits
©2022 Anephenix OÜ. All Rights Reserved.