ExpressBolt is a project aimed at simplifying CRUD operations in Express.js applications. It provides a structured way to handle HTTP requests and responses for creating, reading, updating, and deleting resources in a MongoDB database using Mongoose.
kindly star this project on github expressbolt if you find it helpful
ExpressBolt is a middleware layer built on top of Express.js, a popular web framework for Node.js. It offers a set of utility classes and functions to streamline the development of RESTful APIs with Express and Mongoose.
Key features include:
- CRUD Operations: Provides easy-to-use methods for creating, reading, updating, and deleting resources.
- Error Handling: Implements error handling mechanisms to handle exceptions gracefully.
- Middleware Integration: Seamlessly integrates with Express middleware for request processing.
- Dynamic Schema Generation: Generates Mongoose schemas dynamically based on provided field definitions.
- Query Filtering and Pagination: Supports filtering, sorting, limiting, and pagination of query results.
To use ExpressBolt in your project, follow these steps:
-
Install ExpressBolt via npm:
npm install expressbolt
-
Import ExpressBolt into your Express application:
commons
const express = require('express');
const expressBolt = require('expressbolt');
es2016
import express from 'express'
import expressbolt from 'expressbolt'
- Set up routes and controllers using ExpressBolt classes and functions.
- the crud model creates an easier way to create models using mongoose with strict adherance to all fields that are compulsory using typings as guard
interface UserI {
name: string;
email: string;
password: string;
}
interface UserDocI extends mongoose.Document, UserI {}
interface ModelI {
isEmailTaken(
email: string,
excludeUserId?: mongoose.Types.ObjectId
): Promise<boolean>;
paginate(
filter: Record<string, any>,
options: Record<string, any>
): Promise<any>;
}
- the user model is just an abstract it is no compulsory you use it with the Crud functions
- you can create your own schema and use it as well
import {
CrudController,
CrudService,
generateDynamicSchema,
} from "expressbolt";
import mongoose from "mongoose";
const userModel = generateDynamicSchema<UserI, ModelI>({
modelName: "USER",
fields: {
email: {
type: String,
required: true,
unique: true,
},
name: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
},
schemaOptions: {
timeStamp: true,
},
plugins: [
/* paginate */
],
});
- the model returns
const {model, schema}= userModel
the model is themodel
and theschema
can be extended to suit your schema build ups
- still in consideration to expose the error handler so you can easily catch all your errors
impor {errorCenter} from 'expressbolt';
app.use((err, req, res, next) => {
errorCenter({ error: err, response: res });
});
The CrudController
class is a TypeScript class that provides CRUD operations (create, read, update, delete) for a given model. It takes in a request, response, and next function as parameters in its constructor. The class has methods for creating a new document, creating multiple documents, updating a model, fetching multiple documents, deleting data, and retrieving one document from a database.
- notable mentions:
- the controller has
next, request: req, response: res, env: "production", useNext: false,
as its argument ill explain the one thats not popular here theenv
option takesdevelopment
andproduction
as its argument it will return stack trace error response if its in development, theuseNext
is for us that are lazy to catch our own errors it will return the error message, the response pattern is listed below
- the controller has
- the message returned is either any of the below
function responseMessage<V>(
msg: CustomMessageI<V>,
config: "development" | "production" = "development"
) {
switch (msg.success_status) {
case true:
return {
message: msg.message,
data: msg.data,
success: msg.success_status,
doc_length: msg.doc_length,
};
case false:
return {
message: msg.message,
error: msg.error,
success: msg.success_status,
stack: config === "development" ? msg.stack : {},
};
}
}
export async function getAllUsers(
req: Request,
res: Response,
next: NextFunction
) {
const crud = new CrudController({
next,
request: req,
response: res,
env: "production",
useNext: false,
});
crud.getMany<UserI>({
modelData: { Model: userModel.model, select: ["-password"] },
filter: { name: req.body.name },
populate: {},
query: req.query,
});
}
- Provides CRUD operations (create, read, update, delete) for a given model
- Handles HTTP requests and responses
- Supports error handling and next middleware function invocation
-
create<T, U>({ modelData, data, check })
: Creates a new document in the database using a given model, data, and check query. -
createMany<T, U>({ modelData, data, check })
: Creates multiple documents in the database using a given model, data, and check. -
update<T, U>({ modelData, data, filter })
: Updates a model using the provided data and filter. -
getMany<T>({ modelData, query, populate, filter })
: Fetches multiple documents from the database based on the provided query parameters. -
delete<T>({ modelData, data })
: Deletes data from a model using a filter query. -
getOne<T>({ modelData, data, populate })
: Retrieves one document from a database using a given model, data filter, and optional population fields. -
T
: data type
-
request
: The request object represents the HTTP request made by the client. -
response
: The response object represents the HTTP response that will be sent back to the client. -
next
: The next function is used to pass control to the next middleware function in the request-response cycle. -
useNext
: A boolean flag indicating whether to use the next middleware function. -
env
: The environment (development or production) in which the class is being used. -
modelData
: it contains themodel
and theselect
the select are fields you want or dont want to be returned adding-
to the field will remove it when it returnse.g select:'-password'
will remove the password field -
check
: it checks for fileds you want to be sure its not there before you create -
filter
: will filter the fields you want before you update or get, -
data
: its the payload increate
the search query ingetOne
anddelete
-
populate
: the populate field takes this optionsinterface PopulateFieldI { model?: string; fields?: string; second_layer_populate?: PopulateOptions | string; }
it takes themodel
you want to populate, it must be a key in your object and also it must be a referenced field it takes the fields tou want to retun in that referenced fieldfields
, it takes also subdocument inside the referenced field example structure will now be:
const prod = {
name:"foo",
subCategoryModel:{
name:'foo'
categoryModel:{
name:"foo"
}
}
}
The CrudService
class provides methods for performing CRUD (Create, Read, Update, Delete) operations on a MongoDB database. It includes functions for creating new documents, updating existing documents, fetching data from the database, and deleting documents. The class also includes a method for populating referenced fields in the fetched data.
the crudservice is basically a function that you can reuse here you handle the response and data it returns the value and throws errors if not vali that you have to handle yourself
// Creating a new document
export async function createUser(user: UserI) {
const userCreate = await CrudService.create<UserI>({
check: { email: user.email },
data: user,
modelData: { Model: userModel.model, select: ["-password"] },
});
return userCreate;
}
export async function getUsers(id: mongoose.Types.ObjectId) {
const user = await CrudService.getOne<UserI>({
modelData: { Model: userModel.model, select: ["-password" ]},
data: { _id: id },
populate: {},
});
return user;
}
- Create new documents in the database
- Update existing documents in the database
- Fetch data from the database with filtering, pagination, and sorting options
- Delete documents from the database
- Populate referenced fields in the fetched data
-
create
: Creates a new document in the database and checks if it already exists based on a provided filter query. -
createMany
: Creates multiple documents in the database and performs checks to ensure that the data does not already exist. -
update
: Updates a document in the database based on a filter query. -
getMany
: Fetches multiple documents from the database based on specified criteria and optional population fields. -
populateModel
: Populates fields in a model or query object with referenced documents. -
delete
: Deletes documents from the database based on a filter query. -
getOne
: Retrieves data from the database based on a filter query and optional population fields. -
T
: data type
-
modelData
: Represents the model(s) to be used for CRUD operations. -
data
: Contains the data to be created or updated in the database. -
check
: Specifies the filter query used to check if data already exists in the database. -
filter
: Specifies the filter query used to fetch or update documents in the database. -
populate
: the populate field takes this optionsinterface PopulateFieldI { model?: string; fields?: string; second_layer_populate?: PopulateOptions | string; }
it takes themodel
you want to populate, it must be a key in your object and also it must be a referenced field it takes the fields tou want to retun in that referenced fieldfields
, it takes also subdocument inside the referenced field example structure will now be:
const prod = {
name:"foo",
subCategoryModel:{
name:'foo'
categoryModel:{
name:"foo"
}
}
}
-
modelData
: it contains themodel
and theselect
the select are fields you want or dont want to be returned adding-
to the field will remove it when it returnse.g select:'-password'
will remove the password field -
check
: it checks for fileds you want to be sure its not there before you create -
filter
: will filter the fields you want before you update or get, -
data
: its the payload increate
the search query ingetOne
anddelete
The Queries
class is a TypeScript class that provides methods for filtering, sorting, selecting specific fields, and paginating data in a model object. its not exposed but this is how it looks like
// Create an instance of the Queries class
const queries = new Queries(model, request_query);
// Apply filtering to the model
queries.filter();
// Apply sorting to the model
queries.sort();
// Select specific fields from the model
queries.limitFields();
// Paginate the model
queries.paginate();
The main functionalities of the Queries
class are:
- Filtering the model based on query parameters
- Sorting the model based on the provided sort query parameter
- Selecting specific fields from the model
- Paginating the model
The Queries
class has the following methods:
-
filter()
: Removes excluded fields from the query object and converts it into a string that can be used to filter the model. -
sort()
: Sorts the model based on the provided sort query parameter or defaults to sorting by the created_at field in descending order. -
limitFields()
: Selects specific fields from the model based on thefields
query parameter, or selects all fields except__v
if nofields
parameter is provided. -
paginate()
: Sets the page and limit for pagination in a TypeScript application.
The Queries
class has the following fields:
-
model
: Represents the data structure or schema of the entity that you are working with. -
request_query
: Contains the query parameters from an HTTP request.
This code defines a function called responseMessage
that takes in a custom message object and a configuration option. It returns a response object based on the success status of the message.
const message = {
message: "Success",
success_status: true,
data: { name: "John", age: 25 },
doc_length: 10
};
const response = responseMessage(message);
console.log(response);
// Output: { message: "Success", data: { name: "John", age: 25 }, success: true, doc_length: 10 }
-
msg
(CustomMessageI): An object of typeCustomMessageI<V>
that contains the message, success status, data, and optional properties like stack, error, and doc_length. -
config
(string, optional): A string that specifies the environment in which the function is being executed. It can have two possible values: "development" or "production". By default, it is set to "development".
- The function
responseMessage
takes in a custom message objectmsg
and a configuration optionconfig
. - It checks the value of
msg.success_status
using a switch statement. - If
msg.success_status
is true, it returns an object with propertiesmessage
,data
,success
, anddoc_length
from themsg
object. - If
msg.success_status
is false, it returns an object with propertiesmessage
,error
,success
, andstack
(depending on the value ofconfig
) from themsg
object.
- An object with different properties based on the value of
msg.success_status
. Ifmsg.success_status
is true, it includesmessage
,data
,success
, anddoc_length
properties. Ifmsg.success_status
is false, it includesmessage
,error
,success
, andstack
(depending on the value ofconfig
) properties.
import * as dotenv from "dotenv";
import express, { Express, NextFunction, Request, Response } from "express";
import {
CrudController,
CrudService,
errorCenter,
generateDynamicSchema,
} from "expressbolt";
import mongoose from "mongoose";
import bcrypt from "bcrypt";
dotenv.config();
interface UserI {
name: string;
email: string;
password: string;
}
interface ModelI {
isEmailTaken(
email: string,
excludeUserId?: mongoose.Types.ObjectId
): Promise<boolean>;
paginate(
filter: Record<string, any>,
options: Record<string, any>
): Promise<any>;
}
/* This code snippet is creating a dynamic Mongoose schema for a user model using the
`generateDynamicSchema` function. Here's a breakdown of what each part of the code is doing: */
const userModel = generateDynamicSchema<UserI, ModelI>({
modelName: "USER",
fields: {
email: {
type: String,
required: true,
unique: true,
},
name: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
},
schemaOptions: {
timeStamp: true,
},
plugins: [
/* paginate */
],
});
const { model: USER, schema } = userModel;
schema.pre("save", async function (next) {
this.password = await bcrypt.hash(this.password, 10);
next();
});
/**
* The function `createUser` creates a new user by checking the email, storing user data, and excluding
* the password field.
* @param {UserI} user - The `user` parameter in the `createUser` function is of type `UserI`, which
* likely represents a user object with properties such as `email`, `password`, and other user-related
* information.
* @returns The `createUser` function is returning the result of the `CrudService.create` function,
* which is creating a new user based on the provided `user` object. The `userCreate` variable contains
* the result of the creation operation, and this result is being returned by the `createUser`
* function.
*/
export async function createUser(user: UserI) {
const userCreate = await CrudService.create<UserI>({
check: { email: user.email },
data: user,
modelData: { Model: USER, select: ["-password"] },
});
return userCreate;
}
/**
* This TypeScript function retrieves a user by their ID while excluding the password field.
* @param id - The `id` parameter is of type `mongoose.Types.ObjectId`, which is a unique identifier
* used in Mongoose to represent a specific document in a MongoDB database.
* @returns The `getUsers` function is returning a user object with the specified `_id` and excluding
* the `password` field. The user object is fetched using the `CrudService.getOne` method with the
* provided parameters.
*/
export async function getUsers(id: mongoose.Types.ObjectId) {
const user = await CrudService.getOne<UserI>({
modelData: { Model: USER, select: ["-password"] },
data: { _id: id },
populate: {},
});
return user;
}
/**
* The function `getAllUsers` retrieves multiple user records based on a specified name filter using a
* CrudController instance.
* @param {Request} req - The `req` parameter in the `getAllUsers` function is the request object
* representing the HTTP request made to the server. It contains information about the request such as
* the URL, headers, parameters, body, etc. This parameter is typically provided by the Express.js
* framework when handling incoming HTTP requests.
* @param {Response} res - The `res` parameter in the `getAllUsers` function is an object representing
* the HTTP response that the server sends back to the client. It is used to send data back to the
* client in response to a request.
* @param {NextFunction} next - The `next` parameter in the `getAllUsers` function is a callback
* function that is used to pass control to the next middleware function in the stack. It is typically
* used in Express.js to handle errors or pass control to the next middleware function.
*/
export async function getAllUsers(
req: Request,
res: Response,
next: NextFunction
) {
const crud = new CrudController({
next,
request: req,
response: res,
env: process.env.ENV as "production" | "development",
useNext: false,
});
crud.getMany<UserI>({
modelData: { Model: USER, select: ["email", "name"] },
filter: { name: req.body.name },
populate: {},
query: req.query,
});
}
interface BlogI {
author: mongoose.Types.ObjectId;
category: mongoose.Types.ObjectId;
blog: string;
likes: number;
title: string;
}
interface BlogModelI {
isTitleTaken(
title: string,
excludeBlogId?: mongoose.Types.ObjectId
): Promise<boolean>;
}
interface CategoryI {
name: string;
image: string;
}
interface CategoryModelI {
isNameTaken(
name: string,
excludeCategoryId?: mongoose.Types.ObjectId
): Promise<boolean>;
}
/* The `const categoryModel = generateDynamicSchema<CategoryI, CategoryModelI>({ ... })` code snippet
is creating a dynamic Mongoose schema for a category model using the `generateDynamicSchema`
function. Here's a breakdown of what each part of the code is doing: */
const categoryModel = generateDynamicSchema<CategoryI, CategoryModelI>({
modelName: "CATEGORY",
fields: {
name: {
type: String,
required: true,
},
image: {
type: String,
},
},
schemaOptions: {
timeStamp: true,
},
plugins: [
/* paginate */
],
});
const { model: CATEGORY } = categoryModel;
/* The `const blogModel = generateDynamicSchema<BlogI, BlogModelI>({ ... })` code snippet is creating a
dynamic Mongoose schema for a blog model using the `generateDynamicSchema` function. Here's a
breakdown of what each part of the code is doing: */
const blogModel = generateDynamicSchema<BlogI, BlogModelI>({
modelName: "BLOG",
fields: {
author: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "USER",
},
category: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: CATEGORY,
},
blog: {
type: String,
required: true,
},
likes: {
type: Number,
required: true,
},
title: {
type: String,
},
},
schemaOptions: {
timeStamp: true,
},
plugins: [
/* paginate */
],
});
const { model: BLOG } = blogModel;
/**
* The function `getManyBlog` retrieves multiple blog entries based on the request user's ID and
* specified criteria using a CrudController instance.
* @param {Request} req - The `req` parameter in the `getManyBlog` function is an object representing
* the HTTP request. It contains information about the request made by the client, such as the request
* headers, parameters, body, query parameters, and more. This parameter is typically provided by the
* Express.js framework when handling
* @param {Response} res - The `res` parameter in the function `getManyBlog` is an object representing
* the HTTP response that the server sends back to the client. It is used to send data back to the
* client in response to a request.
* @param {NextFunction} next - NextFunction is a callback function in Express.js that is used to pass
* control to the next middleware function. It is typically used to handle errors or to move to the
* next middleware function in the chain.
*/
function getManyBlog(req: Request, res: Response, next: NextFunction) {
const crud = new CrudController({
next,
request: req,
response: res,
env: process.env.ENV as "production" | "development",
useNext: false,
});
crud.getMany<BlogI>({
modelData: { Model: BLOG, select: ["-__v"] },
filter: { author: req.user.id },
populate: [
{ path: "author", fields: ["name"] },
{ path: "category", fields: ["name", "image"] },
],
query: req.query,
});
}
/**
* The function `deleteBlog` uses a `CrudController` instance to delete a blog entry based on the
* provided ID in the request body.
* @param {Request} req - Request object containing information about the HTTP request
* @param {Response} res - The `res` parameter in the `deleteBlog` function is an object representing
* the HTTP response that the server sends back to the client. It is used to send data back to the
* client, such as status codes, headers, and response body.
* @param {NextFunction} next - NextFunction is a callback function that is used to pass control to the
* next middleware function in the stack. It is typically used in Express.js middleware functions to
* move to the next middleware in the chain.
*/
async function deleteBlog(req: Request, res: Response, next: NextFunction) {
const crud = new CrudController({
next,
request: req,
response: res,
env: process.env.ENV as "production" | "development",
useNext: false,
});
await crud.delete<BlogI>({
modelData: { Model: BLOG, select: ["-__v"] },
data: { _id: req.body.id },
});
}
const app: Express = express();
app.get("/blog", getManyBlog);
app.delete("/blog", deleteBlog);
/* The code snippet `(err: any, req: Request, res: Response, next: NextFunction) => {
errorCenter({
env: process.env.ENV as "production" | "development",
error: err,
response: res,
});
}` is defining an error handling middleware function in an Express application. Here's what it does: */
app.use("/", (err: any, req: Request, res: Response, next: NextFunction) => {
errorCenter({
env: process.env.ENV as "production" | "development",
error: err,
response: res,
});
});
Contributions to ExpressBolt are welcome! To contribute:
- Fork the repository.
- Create a new branch (
git checkout -b feature/improvement
) - Make your changes.
- Commit your changes (
git commit -am 'Add new feature'
) - Push to the branch (
git push origin feature/improvement
) - Create a new Pull Request.
ExpressBolt is licensed under the MIT License. See LICENSE for more information.