AdonisJS - Route Model Binding
January 12, 2020
AdonisJS was built for the NodeJS Artisans taking after the concepts of Laravel - The PHP framework for Artisans. AdonisJS did a great job porting these concepts into JavaScript, it maintains the namespace even though JavaScript does not support that, it using its own fast, easy and extendable view engine and many more, but some features of Laravel are not shipped with AdonisJS by default.
The IoC container in Laravel auto injects classes by inspection when a recognized namespace is Type Hinted. This makes route model binding easier with Laravel. In JavaScript, there is little that can be done to achieve this, so, there is a need for a custom approach to this.
While I believe you have installed AdonisJS and have created an application with the adonis
command, we will begin by creating a resource route:
Route.resource("users","UserController").only("update");
Next we can make a controller for the User
from the terminal using:
adonis make:controller UserController --resource
The above command will create a file /app/Controllers/UserController.js
with predefined methods for a cruddy endpoint. Awesome!
And in the generated controller, lets return the id as JSON
// /app/Controllers/UserController.jsupdate ({request, response}) {return response.json({userId: request.id});}
Now, when we call PUT /users/1
, it should return this:
{"userId": 1}
Interesting…
Traditionally, we would have to lookup the id
against the User
Model, but, we would be doing this everytime for every route and that’s absurd.
How sweet could it be to have a middleware that does that by default, such that we can just call the middleware like so:
Route.resource("users", "UserController").only("update").middleware(new Map([[["update", "boundRouteModel:App/Model/User,user"]]]));
Awesome right?
Let’s do this:
Firstly, let’s make a new middleware:
adonis make:middleware BoundRouteModel --resource
And register it in the /start/kernel.js
, and add the declaration to the nameMiddleware looks this:
// /start/kernel.js// ......const namedMiddleware: {//..."boundRouteModel": "App/Middleware/BoundRouteModelMiddleware"}// ......
Awesome!
Next in the /app/Middleware/BoundRouteModelMiddleware.js
, replace the handle
function with this piece of code.
async handle({ request }, next, [model, identifier, lookupField = "id"]) {if (typeof model === "string") {// Use model directly if a stringmodel = use(model);} else if (Array.isArray(model)) {// Maps through if an arraymodel = model.map(async _model => {// Model is a string, use directlyif (typeof _model === "string") {return use(_model);} else if (_model.findByOrFail &&typeof _model.findByOrFail === "function") {// Model already contains an already imported modelreturn _model;}// _model is not recognized by this code as usable or callable for its usagereturn null;});} else if (model && !model.$booted) {// model is defined from the middleware arguments but not actually a Modelawait next();}if (model && typeof model !== "string") {const params = request.params;Object.keys(params).map(async (paramKey, index) => {let modelForParam = model;// Get the model at the index if the model is an arrayif (Array.isArray(model)) {modelForParam = model[index];}if (modelForParam && modelForParam.$booted) {const paramValue = params[paramKey];request[identifier] = await modelForParam.findByOrFail(lookupField, // The field to use for the lookupparamValue // Value passed from the request);}});}// call next to advance the requestawait next();}
The code above is doing pretty lot of things.
First it assumes that the model passed to the middleware is a string.
Then it attemps to import it from the IoC container, but if the model is an array, it loops through the array checking if the element is a string and tries to import it, or use it directly if it is an alreay imported Model.
But if the model is defined and is not recognized as a Model or an Array, the middleware is passed on to the next.
Finally, the actual look up starts by checking if the model is not a string, picks up every item from the request parameter and assign a model at the index of the request parameter to it if model is an array, else if the model is a Model, it just looks it up and stores the result in the request using the specified identifier in the middleware initialization.
Yes, that is all to it.
When next you want to use route model binding, you just use the middleware.
Conclusion
It might feel like a heck of a task, but to me it is worth it. There is a library for this purpose and it uses the provider technique, you might want to check it out if you don’t like this approach.
Thanks for reading. 🙇🙇🙇
Edit on githubTweet