xprevalent is a library for creating express controllers. Define a controller and use with express router. Controllers are declarative and divides request handling into many stages.
npm install xprevalent
- Generic Controller
- How to Use with Express Router
- Validator
- List Controller
- Details Controller
- Create Controller
- Update Controller
- Delete Controller
Controller is a generic class that handles the request and returns a response.
There are four types of controllers:
type | description |
---|---|
list | returns list of records (optionally paginated) |
details | returns single record |
create | create a new record in database |
update | update a record in database |
delete | deletes a record from database |
All the controllers share the following params:
name | type | description |
---|---|---|
req |
object | express req object |
res |
object | express response object |
next |
function | express next function |
model |
object | mongoose model |
queryObjectName |
string | the name of the field in response representing the data returned from controller |
selectedFields |
array of strings | used by mongoose model to select certain fields |
excludedFields |
array of strings | used by mongoose model to exclude certain fields |
sortBy |
object | used by mongoose model to sort the result |
populatedFields |
array | used by mongoose model to populate fields |
There are some common methods that is shared by all the controllers:
name | input | returns | description |
---|---|---|---|
getContextObject |
object | used to return any additional data with response body | |
getQueryResult |
object or array of objects | used to fetch/change data in database and returns the query result that will be returned in the response body | |
sendResponse |
type = "json" or "generic", success = boolean, status = status code, message = string, error = object, body = object represent response body | express response | used to return a response based on the given input |
handleRequest |
a response or calls the next handler | used to handle the request and returns a response or either uses next() to pass the error to the next handler |
In order to use the controller with express router, all you have to do is to pass Controller.handle
as the request handler.
const { ApiListController } = require("xprevalent");
const userModel = require("models/user");
class UsersListController extends ApiListController {
model = userModel;
paginate = true;
}
module.exports = UsersListController;
const router = require("express").Router();
const UsersListController = require("controllers/users.list.controller");
router.get("/users", UsersListController.handle);
module.exports = router;
The validator is used to validate all the data passed to the controller.
To use the validator you must create a validation schema using ValidationSchema
class and pass a schema
to it.
const { ValidationSchema, schema } = require("xprevalent");
const userSchema = {
body: schema.object({
email: schema.string().email().required()
})
};
const validationSchema = new ValidationSchema(userSchema);
The schema is an object that consists of three different keys. each key represent a request location:
- params
- query
- body
The value of params, query and body is based on yup validator library.
const { ValidationSchema, schema } = require("xprevalent");
const userSchema = {
params: schema.object({
id: schema.number().integer().positive().required()
}),
query: schema.object({
name: schema.string().required()
}),
body: schema.object({
email: schema.string().email().required()
})
};
const validationSchema = new ValidationSchema(userSchema);
In order to use validator in controllers, all you have to do is to set validation schema. and the controller will handle the validation automatically.
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema({
body: schema.object({
email: schema.string().email().required()
})
});
}
It is better to create schemas into its own separate files and export it into controller.
// schemas/users.schema.js
const { schema } = require("xprevalent");
const updateUserSchema = {
body: schema.object({
email: schema.string().email().required()
})
};
const { ValidationSchema } = require("xprevalent");
const { updateUserSchema } = require("schemas/users.schema");
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema(updateUserSchema);
}
Validation schema validates the input using validate()
which takes in the request object and validates it based on the different locations used in the validation schema.
The validationSchema.validate(req)
returns an object that contains three methods:
hasError
: takes in an optional object consists of{ name, location }
and returns boolean based on whether the request has an error or not. the location is one of three stringsparams, query, body
if the location is specified it will check if the location has any error in one of its fields. and if the name is specified, it will check iflocation[name]
has an error, if none of them is specified it will check if the schema has an error in any of the three locations.getValue
: takes in an optional parametername
and returns the validated value of that field specified byreq[name]
. if name is not specified it will return an object consists of the three locations{ params, query, body }
.getErrors
: takes in an optional object consists of{ name, location }
and returns error object. the location is one of three stringsparams, query, body
if the location is specified it will return the location errors. and if the name is specified, it will returnlocation[name]
errors, if none of them is specified it will return all the errors in all of the three locations.
// schemas/users.schema.js
const { ValidationSchema, schema } = require("xprevalent");
const updateUserSchema = {
body: schema.object({
email: schema.string().email().required(),
name: schema.string().required()
})
};
const validationSchema = new ValidationSchema(updateUserSchema);
const validationResult = validationSchema.validate({
body: {
email: "",
name: "Mohamed"
}
});
// will return true as email is invalid
console.log(validationResult.hasError());
// will return true as body[email] is invalid
console.log(validationResult.hasError({ location: "body" }));
// will return true as email is invalid
console.log(validationResult.hasError({ location: "body", name: "email" }));
// will return false as body[name] is valid
console.log(validationResult.hasError({ location: "body", name: "name" }));
// will return {} as body is invalid
console.log(validationResult.getValue());
// will return undefined as body is invalid
console.log(validationResult.getValue("body"));
// will return { body: { email: "email is a required field" } }
console.log(validationResult.getErrors());
// will return { email: "email is a required field" }
console.log(validationResult.getErrors({ location: "body" }));
// will return "email is a required field"
console.log(validationResult.getErrors({ location: "body", name: "email" }));
// will return undefined as name is valid
console.log(validationResult.getErrors({ location: "body", name: "name" }));
Used To list all documents optionally paginated.
class UsersListController extends ApiListController {
model = userModel;
}
To list documents with pagination; set paginate to true
.
class UsersListController extends ApiListController {
model = userModel;
paginate = true;
}
By default the controller will get the pagination parameters from request query page
, limit
and if neither of them is provided will use a default page = 1
and limit = 25
.
To override the default parameters, you can set paginateBy
object.
class UsersListController extends ApiListController {
model = userModel;
paginate = true;
paginateBy = {
// use req.query.cursor to get page number
pageParam: "cursor",
// use req.query.offset to get the limit
limitParam: "offset",
// use a default limit of 30 if limit is not provided
defaultLimit: 30
};
}
By default the controller will get the document count using getDocumentsCount()
and will use getQueryFilter()
as the filter used to count documents. you can override getDocumentsCount()
to control how the documents is counted.
class UsersListController extends ApiListController {
...
async getDocumentsCount() {
const count = await this.model.countDocuments({ deletedAt: { $eq: null } });
return count;
}
}
By default the controller will return a pagination object in the response with the following structure:
return { count, totalPages, page, limit, nextPage, previousPage };
To control what data is returned in this object you can override getPaginationMeta()
.
class UsersListController extends ApiListController {
...
async getPaginationMeta() {
const count = await this.getDocumentsCount();
const { page, limit } = this.getPaginationParams();
const lastPage = Math.ceil(count / limit);
// need to set this.totalPages as it will be used in getQueryResult
this.totalPages = lastPage > 0 ? lastPage : 1;
const meta = {
count,
totalPages: lastPage,
page: page < lastPage ? page : this.totalPages,
limit,
nextPage: page < lastPage ? page + 1 : null,
previousPage:
page > 1 ? (page < lastPage ? page - 1 : lastPage - 1) : null
};
return meta;
}
}
To control the filter that is used to count documents and to fetch data from database, you can override getQueryFilter()
.
class UsersListController extends ApiListController {
...
async getQueryFilter() {
return {
isActive: { $eq: Boolean(this.req.query.isActive) }
}
}
}
To control how data is fetched from database you can override getQueryResult()
.
class UsersListController extends ApiListController {
...
getQueryResult() {
const { page, limit } = this.getPaginationParams();
return this.model.aggregate([
{
$skip: (page - 1) * limit
},
{
$limit: limit
}
...
]);
}
}
To return any additional data with the response you can override getContextObject()
.
class UsersListController extends ApiListController {
...
async getContextObject() {
const context = await super.getContextObject();
// get any data you want to return in response
const additionalData = this.getAdditionalData();
return {
...context,
...additionalData
};
}
}
Details controller is used to get a single object.
By default this object will be fetched by id and will use req.params.id
to fetch the object from database.
class UserDetailsController extends ApiDetailsController {
model = userModel;
}
To control the parameter used to fetch the document from database you can override idParam
.
class UserDetailsController extends ApiDetailsController {
model = userModel;
// this will use req.params.userId to fetch the document from database
idParam = "userId";
}
To controller how the id is validated you can override idParamIsInvalid()
and return a boolean indicating if the id is valid or not.
If the id is not valid the controller will return 404
.
class UserDetailsController extends ApiDetailsController {
...
idParamIsInvalid() {
const id = this.req.params[this.idParam];
return !Number.isInteger(parseInt(id));
}
}
To get a single document using query filter, you can set findOne
to true and override getQueryFilter()
to return the filter used to get the document.
class UserDetailsController extends ApiDetailsController {
model = userModel;
findOne = true;
getQueryFilter() {
return {
name: { $regex: /Mohamed/, $options: "i" }
};
}
}
To control how data is fetched from database you can override getQueryResult()
.
class UserDetailsController extends ApiDetailsController {
...
getQueryResult() {
const id = this.req.params[this.idParam];
return this.model.findById(id);
}
}
To return any additional data with the response you can override getContextObject()
.
class UserDetailsController extends ApiDetailsController {
...
async getContextObject() {
const context = await super.getContextObject();
// get any data you want to return in response
const additionalData = this.getAdditionalData();
return {
...context,
...additionalData
};
}
}
Create controller is used to insert document(s) into database.
The controller will first validate the request before inserting new documents using validation schema.
class UserCreateController extends ApiCreateController {
model = userModel;
validationSchema = new ValidationSchema(createUserSchema);
}
By default the inserted document will be the result of the validation returned from validating the request body.
If the data is not valid 422
status code will be returned with error object in the response.
To customize the inserted document or control the values of the document fields, you can override getDocument()
.
class UserCreateController extends ApiCreateController {
...
getDocument() {
const user = this.validationResult.getValue("body");
return {
...user,
role: "default"
};
}
}
To control how data is inserted into database you can override getQueryResult()
.
class UserCreateController extends ApiCreateController {
...
getQueryResult() {
const document = this.getDocument();
return this.model.create(document);
}
}
By default the created document will be returned in the response.
To return any additional data with the response you can override getContextObject()
.
class UserCreateController extends ApiCreateController {
...
async getContextObject() {
const context = await super.getContextObject();
// get any data you want to return in response
const additionalData = this.getAdditionalData();
return {
...context,
...additionalData
};
}
}
Update controller is used to update document(s) into database.
The controller will first validate the request before updating documents using validation schema.
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema(updateUserSchema);
}
To control the parameter used to fetch the document from database you can override idParam
.
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema(updateUserSchema);
// this will use req.params.userId to fetch the document from database
idParam = "userId";
}
To controller how the id is validated you can override idParamIsInvalid()
and return a boolean indicating if the id is valid or not.
If the id is not valid the controller will return 404
.
class UserUpdateController extends ApiUpdateController {
...
idParamIsInvalid() {
const id = this.req.params[this.idParam];
return !Number.isInteger(parseInt(id));
}
}
By default the updated document will be the result of the validation returned from validating the request body.
The controller only updates the values contained in the request body.
If the data is not valid 422
status code will be returned with error object in the response.
To customize the updated document or control the fields to be updated, you can override getUpdateSet()
.
class UserUpdateController extends ApiUpdateController {
...
getUpdateSet() {
const updateSet = this.validationResult.getValue("body");
return {
...updateSet,
role: "default"
};
}
}
To update a single document using query filter, you can set updateOne
to true and override getQueryFilter()
to return the filter used to get the document to be updated.
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema(updateUserSchema);
updateOne = true;
getQueryFilter() {
return {
name: { $regex: /Mohamed/, $options: "i" }
};
}
}
By setting upsert
to true
if the document is not found in database, it will be created.
class UserUpdateController extends ApiUpdateController {
model = userModel;
validationSchema = new ValidationSchema(updateUserSchema);
upsert = true;
}
To control how data is updated you can override getQueryResult()
.
class UserUpdateController extends ApiUpdateController {
...
getQueryResult() {
const updateSet = this.getUpdateSet();
const id = this.req.params[this.idParam];
return this.model.findByIdAndUpdate(id, updateSet, { new: true, upsert: true });
}
}
By default the newly updated document will be returned in the response.
To return any additional data with the response you can override getContextObject()
.
class UserUpdateController extends ApiUpdateController {
...
async getContextObject() {
const context = await super.getContextObject();
// get any data you want to return in response
const additionalData = this.getAdditionalData();
return {
...context,
...additionalData
};
}
}
Delete controller is used to delete document(s) from database.
To control the parameter used to fetch the document from database you can override idParam
.
class UserDeleteController extends ApiDeleteController {
model = userModel;
// this will use req.params.userId to fetch the document from database
idParam = "userId";
}
To controller how the id is validated you can override idParamIsInvalid()
and return a boolean indicating if the id is valid or not.
If the id is not valid the controller will return 404
.
class UserDeleteController extends ApiDeleteController {
...
idParamIsInvalid() {
const id = this.req.params[this.idParam];
return !Number.isInteger(parseInt(id));
}
}
To delete a single document using query filter, you can set deleteOne
to true
and override getQueryFilter()
to return the filter used to get the document to be deleted.
class UserDeleteController extends ApiDeleteController {
model = userModel;
deleteOne = true;
getQueryFilter() {
return {
name: { $regex: /Mohamed/, $options: "i" }
};
}
}
To control how the document is deleted you can override getQueryResult()
.
class UserDeleteController extends ApiDeleteController {
...
getQueryResult() {
const id = this.req.params[this.idParam];
return this.model.findByIdAndDelete(id);
}
}
By default the deleted document will be returned in the response.
To return any additional data with the response you can override getContextObject()
.
class UserDeleteController extends ApiDeleteController {
...
async getContextObject() {
const context = await super.getContextObject();
// get any data you want to return in response
const additionalData = this.getAdditionalData();
return {
...context,
...additionalData
};
}
}