ezy-response
Version:
Simplified and standardized Express.js response handling with clean, one-liner syntax for success, error, custom, and stream-based responses
382 lines (331 loc) • 10.2 kB
JavaScript
/**
* ezy-response - Simplified Express.js response handling
* A clean, one-liner syntax for sending success, error, custom, or stream-based responses
*
* @author Bittu-the-coder
* @version 1.0.0
*/
const compression = require('compression');
/**
* Enhanced response object with ezy-response methods
* @typedef {Object} EzyResponse
* @extends {import('express').Response}
*/
/**
* Success response options
* @typedef {Object} SuccessOptions
* @property {string} [message] - Success message
* @property {any} [data] - Response data
* @property {Object} [meta] - Additional metadata
* @property {number} [statusCode=200] - HTTP status code
*/
/**
* Error response options
* @typedef {Object} ErrorOptions
* @property {string} [message] - Error message
* @property {any} [error] - Error details
* @property {string} [code] - Error code
* @property {number} [statusCode=500] - HTTP status code
* @property {Object} [meta] - Additional metadata
*/
/**
* Custom response options
* @typedef {Object} CustomOptions
* @property {number} statusCode - HTTP status code
* @property {any} data - Response data
* @property {Object} [headers] - Additional headers
*/
/**
* Pagination metadata
* @typedef {Object} PaginationMeta
* @property {number} page - Current page
* @property {number} limit - Items per page
* @property {number} total - Total items
* @property {number} totalPages - Total pages
* @property {boolean} hasNext - Has next page
* @property {boolean} hasPrev - Has previous page
*/
/**
* Send a standardized success response
* @param {SuccessOptions} options - Success response options
* @returns {import('express').Response}
*/
function success(options = {}) {
const { message = 'Success', data = null, meta = {}, statusCode = 200 } = options;
const response = {
success: true,
message,
...(data !== null && { data }),
...(Object.keys(meta).length > 0 && { meta }),
timestamp: new Date().toISOString()
};
return this.status(statusCode).json(response);
}
/**
* Send a standardized error response
* @param {ErrorOptions} options - Error response options
* @returns {import('express').Response}
*/
function error(options = {}) {
const {
message = 'An error occurred',
error = null,
code = 'INTERNAL_ERROR',
statusCode = 500,
meta = {}
} = options;
const response = {
success: false,
message,
code,
...(error !== null && { error }),
...(Object.keys(meta).length > 0 && { meta }),
timestamp: new Date().toISOString()
};
return this.status(statusCode).json(response);
}
/**
* Send a custom response
* @param {CustomOptions} options - Custom response options
* @returns {import('express').Response}
*/
function custom(options) {
const { statusCode, data, headers = {} } = options;
// Set additional headers if provided
Object.keys(headers).forEach(key => {
this.set(key, headers[key]);
});
return this.status(statusCode).json(data);
}
/**
* Send a paginated success response
* @param {Object} options - Pagination options
* @param {any} options.data - Response data
* @param {PaginationMeta} options.pagination - Pagination metadata
* @param {string} [options.message] - Success message
* @returns {import('express').Response}
*/
function paginated(options) {
const { data, pagination, message = 'Data retrieved successfully' } = options;
const response = {
success: true,
message,
data,
pagination,
timestamp: new Date().toISOString()
};
return this.status(200).json(response);
}
/**
* Send a no content response
* @param {string} [message] - Optional message
* @returns {import('express').Response}
*/
function noContent(message) {
if (message) {
return this.status(200).json({ message, timestamp: new Date().toISOString() });
}
return this.status(204).send();
}
/**
* Send a created response
* @param {Object} options - Created response options
* @param {any} options.data - Created resource data
* @param {string} [options.message] - Success message
* @param {string} [options.location] - Location header for the created resource
* @returns {import('express').Response}
*/
function created(options) {
const { data, message = 'Resource created successfully', location } = options;
if (location) {
this.set('Location', location);
}
const response = {
success: true,
message,
data,
timestamp: new Date().toISOString()
};
return this.status(201).json(response);
}
/**
* Send a validation error response
* @param {Object} options - Validation error options
* @param {Array} options.errors - Array of validation errors
* @param {string} [options.message] - Error message
* @returns {import('express').Response}
*/
function validationError(options) {
const { errors, message = 'Validation failed' } = options;
const response = {
success: false,
message,
code: 'VALIDATION_ERROR',
errors,
timestamp: new Date().toISOString()
};
return this.status(422).json(response);
}
/**
* Send an unauthorized response
* @param {string} [message] - Error message
* @returns {import('express').Response}
*/
function unauthorized(message = 'Unauthorized access') {
const response = {
success: false,
message,
code: 'UNAUTHORIZED',
timestamp: new Date().toISOString()
};
return this.status(401).json(response);
}
/**
* Send a forbidden response
* @param {string} [message] - Error message
* @returns {import('express').Response}
*/
function forbidden(message = 'Access forbidden') {
const response = {
success: false,
message,
code: 'FORBIDDEN',
timestamp: new Date().toISOString()
};
return this.status(403).json(response);
}
/**
* Send a not found response
* @param {string} [message] - Error message
* @returns {import('express').Response}
*/
function notFound(message = 'Resource not found') {
const response = {
success: false,
message,
code: 'NOT_FOUND',
timestamp: new Date().toISOString()
};
return this.status(404).json(response);
}
/**
* Send a conflict response
* @param {string} [message] - Error message
* @returns {import('express').Response}
*/
function conflict(message = 'Resource conflict') {
const response = {
success: false,
message,
code: 'CONFLICT',
timestamp: new Date().toISOString()
};
return this.status(409).json(response);
}
/**
* Send a rate limit exceeded response
* @param {Object} [options] - Rate limit options
* @param {string} [options.message] - Error message
* @param {number} [options.retryAfter] - Retry after seconds
* @returns {import('express').Response}
*/
function rateLimitExceeded(options = {}) {
const { message = 'Rate limit exceeded', retryAfter = 60 } = options;
if (retryAfter) {
this.set('Retry-After', retryAfter.toString());
}
const response = {
success: false,
message,
code: 'RATE_LIMIT_EXCEEDED',
retryAfter,
timestamp: new Date().toISOString()
};
return this.status(429).json(response);
}
/**
* Stream a file as response
* @param {Object} options - Stream options
* @param {string} options.filePath - Path to the file
* @param {string} [options.filename] - Download filename
* @param {string} [options.mimeType] - MIME type
* @param {boolean} [options.inline=false] - Whether to display inline or as attachment
* @returns {import('express').Response}
*/
function streamFile(options) {
const { filePath, filename, mimeType, inline = false } = options;
if (filename) {
const disposition = inline ? 'inline' : 'attachment';
this.set('Content-Disposition', `${disposition}; filename="${filename}"`);
}
if (mimeType) {
this.set('Content-Type', mimeType);
}
return this.sendFile(filePath);
}
/**
* Send a server error response
* @param {string} [message] - Error message
* @param {any} [error] - Error details (only included in development)
* @returns {import('express').Response}
*/
function serverError(message = 'Internal server error', error = null) {
const response = {
success: false,
message,
code: 'INTERNAL_SERVER_ERROR',
timestamp: new Date().toISOString()
};
// Only include error details in development environment
if (process.env.NODE_ENV === 'development' && error) {
response.error = error;
}
return this.status(500).json(response);
}
/**
* Middleware to add ezy-response methods to Express response object
* @param {Object} [options] - Middleware options
* @param {boolean} [options.enableCompression=false] - Enable gzip compression
* @returns {Function} Express middleware
*/
function ezyResponse(options = {}) {
const { enableCompression = false } = options;
return (req, res, next) => {
// Add all ezy-response methods to the response object
res.success = success.bind(res);
res.error = error.bind(res);
res.custom = custom.bind(res);
res.paginated = paginated.bind(res);
res.noContent = noContent.bind(res);
res.created = created.bind(res);
res.validationError = validationError.bind(res);
res.unauthorized = unauthorized.bind(res);
res.forbidden = forbidden.bind(res);
res.notFound = notFound.bind(res);
res.conflict = conflict.bind(res);
res.rateLimitExceeded = rateLimitExceeded.bind(res);
res.streamFile = streamFile.bind(res);
res.serverError = serverError.bind(res);
// Enable compression if requested
if (enableCompression) {
return compression()(req, res, next);
}
next();
};
}
// Export the middleware and individual functions
module.exports = ezyResponse;
module.exports.success = success;
module.exports.error = error;
module.exports.custom = custom;
module.exports.paginated = paginated;
module.exports.noContent = noContent;
module.exports.created = created;
module.exports.validationError = validationError;
module.exports.unauthorized = unauthorized;
module.exports.forbidden = forbidden;
module.exports.notFound = notFound;
module.exports.conflict = conflict;
module.exports.rateLimitExceeded = rateLimitExceeded;
module.exports.streamFile = streamFile;
module.exports.serverError = serverError;