Routes
Routes are relative to the controller route. You SHOULD NOT use the full route here.
Route objects have multiple levels.
Route First Level (Method Level)
On the first level, only the ‘method’ (post, put, delete, etc.) exists. Only requests with these methods will go deeper into the real routes.
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
class ControllerName extends AbstractController {
get routes() {
return {
post: {
// post routes
},
get: {
// get routes
},
put: {
// put routes
},
// etc.
};
}
}
export default ControllerName;
Route Second Level (Path Level)
Inside the methods (second level), we have a path. It follows the https://expressjs.com/en/guide/routing.html#route-paths Express documentation.
In most cases, a few options are enough:
"/fullpath";
// Grab variables paramOne and paramTwo into req.params
"/fullpath/:paramOne/:paramTwo";
// Like the previous one, but "paramTwo" is now optional
"/fullpath/:paramOne/{:paramTwo}";
The order of routes matters. The first matched route will be executed.
Example:
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
class ControllerName extends AbstractController {
get routes() {
return {
post: {
"/someUrl": {
handler: this.postSomeUrl,
request: yup.object().shape({
count: yup.number().max(100).required(),
}),
},
},
};
}
}
export default ControllerName;
Route Third Level (Route Object Level)
On the third level, we have a "route object," a special object that will describe our route.
{
handler: this.postSomeUrl, // required
request: yup.object().shape({ // optional
count: yup.number().max(100).required(),
}),
query: yup.object().shape({ // optional
page: yup.number().required(),
}),
middleware: [RateLimiter], // optional
description: yup.string() // optional
}
Here:
Handler; // Some async function (most likely in this controller file) that will do all the work.
Request; // A special interface that will do validation of body parameters for you.
Query; // A special interface that will do validation of query parameters for you.
Middleware; // An array of middlewares specially for the current route.
Description; // A description of this route (used when generating documentation).
Request
Request does validation and casting of an upcoming req.body
.
As we want to use already well-defined solutions, we believe that yup is a great example of how a schema should be validated.
But you still have the ability to provide your own validation based on an interface.
Request works on a body level.
Request contains all fields from req.body
and passes them into validation.
Please note that GET methods have no BODY.
Parameters after validation are available as req.appInfo.request
.
Do not use req.body
directly. Always use parameters via req.appInfo.request
.
Query
Query does validation and casting of an upcoming req.query
.
The Yup schema is described similarly to the request.
Query works on a query level.
Query contains all fields from req.query
and passes them into validation.
Parameters after validation are available as req.appInfo.query
.
Do not use req.query
directly. Always use parameters via req.appInfo.query
.
Yup Validation
request: yup.object().shape({
count: yup.number().max(100).required("error text"),
});
query: yup.object().shape({
page: yup.number(),
});
Please follow the Yup documentation for a deeper understanding of how schemas work. All parameters are located here: https://github.com/jquense/yup#api.
Example:
request: yup.object().shape({
name: yup.string().required("validation.name"), // You can use i18n keys here.
email: yup.string().email().required("Email required field"), // Or just text.
message: yup
.string()
.required("Message required field")
.min(30, "minimum 30 chars"), // Additional validators for different types exist.
pin: yup.number().integer().min(1000).max(9999).required("pin.pinProvided"),
status: yup
.string()
.required("Status required field")
.oneOf(["WAITING", "CANCELED"]), // One of.
transaction: yup
.object() // Deep-level object.
.shape({
to: yup.string().required(),
amount: yup.number().required(),
coinName: yup.string().oneOf(["btc", "etc"]).default("etc"), // Default.
})
.required(),
});
File Validation
For file validation, we provide a special Yup class, YupFile
. It is really simple to use.
import { YupFile } from "@adaptivestone/framework/helpers/yup.js";
request: yup.object().shape({
someFileName: new YupFile().required("error text"),
otherFiled: yup.string().required(), // Yes, you can mix it with regular data.
});
Please be aware that a file can only be uploaded by ‘multipart/form-data’, and because of this, you can’t use nested objects.
Own Validation
To create your own validator, your object should have two methods:
async validate(req.body) // Throw an error on validation failed.
async cast(req.body) // Should strip unknown parameters.
The error object should provide an “errors” array - error (why validation failed) and a "path" string - body parameter.
try {
await request.validate(req.body);
} catch (e) {
// e.path
// e.errors
}
req.appInfo.request = request.cast;
i18n
In any fields that can generate an error (required, etc.), you can use i18n keys to translate. The framework will handle the translation for you.
Please refer to the i18n documentation.
Handler
Handler - some async function (most likely in this controller file) that will do all the work. It is better to write the function in the same file.
The handler can only be an async function.
req.appInfo.app
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
class ControllerName extends AbstractController {
get routes() {
return {
post: {
'/': {
handler: this.postSample,
request: yup.object().shape({
count: yup.number().max(100).required(),
})
}
}
}
}
// Send a request with data {count: "5000"}.
// Will produce an error with status 400 and {errors: {count:['Text error']}}.
postSample(req, res) {
// On success validation, we pass here.
// {count: "5000"}
console.log(req.appInfo.request)
// {count: 5000} -> casted to a number
const SomeModel = this.app.getModel('SomeModel');
const SomeModelAlternativeWay = req.appInfo.app.getModel('SomeModel');
const { count } = req.appInfo.request;
const someModel = await SomeModel.findOne({count});
return res.status(200).json({modelId: someModel.id});
}
}
export default ControllerName;
Middleware
Middleware - an array of middlewares specially for the current route.
Route middlewares take precedence over middlewares in controllers.
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
class ControllerName extends AbstractController {
get routes() {
return {
get: {
'/routeName': {
handler: ...,
middleware: [MiddlewareName, MiddlewareName, etc]
}
},
};
}
}
export default ControllerName;
Similarly to controller middlewares, you can use middlewares with parameters.
The rules for the design of middlewares with parameters are described in the subsection "Middleware".
Sample:
import AbstractController from "@adaptivestone/framework/modules/AbstractController.js";
import RoleMiddleware from "@adaptivestone/framework/services/http/middleware/Role.js";
class ControllerName extends AbstractController {
get routes() {
return {
get: {
'/routeName': {
handler: ...,
middleware: [[RoleMiddleware, { roles: ['client'] }]]
}
},
};
}
}
export default ControllerName;