postgre-pagination
v1.0.3
Published
A lightweight PostgreSQL pagination for TypeScript
Maintainers
Readme
postgre-pagination
A powerful utility library for Express route development
Simplify pagination, search, and error handling to maximize Express API development productivity.
✨ Features
- 🚀 Express Route Optimization - Eliminate code duplication and improve readability with middleware-based architecture
- 🔒 Full Type Safety - Complete type inference using TypeScript generics
- 📄 Automatic Pagination - Handle validation to response in one go
- 🔍 Advanced Search - Support for LIKE, ILIKE, and Full-Text Search
- 🛡️ Comprehensive Error Handling - Structured error classes with automatic error responses
- 🚦 Rate Limiting - Request throttling with PostgreSQL integration
- 📊 Consistent API Responses - Unified response format for better client-side development
- ⚡ Zero Configuration - Ready-to-use default settings
📦 Installation
npm install postgre-pagination
# or
yarn add postgre-pagination
# or
pnpm add postgre-pagination🚀 Quick Start
Environment Setup
Create a .env file in your project root:
# Database Configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=mydb
DB_USER=postgres
DB_PASSWORD=yourpasswordBasic Usage
import express, { Request, Response } from "express";
import {
paginationMiddleware,
catchAsyncErrors,
Respond,
NotFoundError,
} from "postgre-pagination";
const app = express();
const postsRouter = express.Router();
// Route with pagination and error handling
postsRouter.get(
"/",
paginationMiddleware({ limit: 10, page: 1 }),
catchAsyncErrors(async (req: Request, res: Response) => {
const { page, limit } = req.pagination;
const result = await postsService.read({ isValid: true }, { page, limit });
return Respond.ok(res, result);
})
);
// Single post retrieval - automatic error handling
postsRouter.get(
"/:id",
catchAsyncErrors(async (req: Request, res: Response) => {
const post = await postsService.findById(req.params.id);
if (!post) {
throw new NotFoundError("Post not found");
}
return Respond.ok(res, post);
})
);
app.use("/posts", postsRouter);📚 API Documentation
1. Pagination
paginationMiddleware(customDefaults?)
Automatically extracts and validates pagination information from request query parameters.
Parameters:
customDefaults(optional) - Custom default valuespage: Default page number (default: 1)limit: Items per page (default: 10)
Usage:
import {
paginationMiddleware,
catchAsyncErrors,
Respond,
} from "postgre-pagination";
// Using default values
router.get(
"/users",
paginationMiddleware(),
catchAsyncErrors(async (req, res) => {
const { page, limit } = req.pagination;
// page: 1, limit: 10 (default)
const users = await userService.findAll({ page, limit });
return Respond.ok(res, users);
})
);
// Using custom default values
router.get(
"/products",
paginationMiddleware({ page: 1, limit: 20 }),
catchAsyncErrors(async (req, res) => {
const { page, limit } = req.pagination;
// page: 1, limit: 20 (custom default)
const products = await productService.findAll({ page, limit });
return Respond.ok(res, products);
})
);Request Example:
GET /users?page=2&limit=15Validation Rules:
page: Integer greater than or equal to 1limit: Integer between 1-100 (default range, configurable)
validatePaginationParams(req, customDefaults?)
Utility function for manually validating pagination parameters.
import { validatePaginationParams } from "postgre-pagination";
const validation = validatePaginationParams(req);
if (!validation.isValid) {
return res.status(validation.error.status).json({
message: validation.error.message,
});
}
const { page, limit } = validation.params;2. Search
searchMiddleware<T>(allowedFields, defaultFields?)
Extracts and validates search information from request query parameters.
Parameters:
allowedFields: List of searchable fieldsdefaultFields(optional): Default search fields
Usage:
import {
searchMiddleware,
paginationMiddleware,
catchAsyncErrors,
Respond,
} from "postgre-pagination";
interface Post {
id: number;
title: string;
content: string;
author: string;
}
router.get(
"/posts/search",
paginationMiddleware(),
searchMiddleware<Post>(
["title", "content", "author"], // Allowed fields
["title", "content"] // Default fields
),
catchAsyncErrors(async (req, res) => {
const { page, limit } = req.pagination;
const searchOptions = req.search;
const posts = await postService.search(searchOptions, { page, limit });
return Respond.ok(res, posts);
})
);Request Example:
GET /posts/search?search=typescript&searchFields=title,content&searchMode=ilikeQuery Parameters:
search: Search termsearchFields: Fields to search (comma-separated)searchMode: Search modelike: Case-sensitive searchilike: Case-insensitive search (default)fts: Full-Text Search
caseSensitive: Case sensitivity (true/false)multiWord: Multi-word search support (true/false)
validateSearchParams<T>(req, allowedFields, defaultFields?)
Utility function for manually validating search parameters.
import { validateSearchParams } from "postgre-pagination";
const validation = validateSearchParams<Post>(req, ["title", "content"]);
if (!validation.isValid) {
return res.status(validation.error.status).json({
message: validation.error.message,
});
}
const searchOptions = validation.params;3. Error Handling
catchAsyncErrors(fn, options?)
Automatically catches and handles errors in async route handlers.
Parameters:
fn: Async route handler functionoptions(optional): Error handling optionslogger: Custom logging functionerrorTransformer: Error response transformation functionshowStackTrace: Show stack trace (auto-enabled in development)notifyError: Error notification function (Slack, Email, etc.)collectMetrics: Error metrics collection function
Usage:
import {
catchAsyncErrors,
NotFoundError,
ValidationError,
Respond,
} from "postgre-pagination";
// Basic usage
router.get(
"/users/:id",
catchAsyncErrors(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User not found");
}
return Respond.ok(res, user);
})
);
// With custom options
router.post(
"/users",
catchAsyncErrors(
async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
throw new ValidationError("Email and password are required");
}
const user = await userService.create({ email, password });
return Respond.created(res, user);
},
{
logger: (error, req) => {
console.error(`[API Error] ${req.method} ${req.path}:`, error.message);
},
notifyError: async (error, req) => {
await slackNotifier.send(`Error on ${req.path}: ${error.message}`);
},
}
)
);Error Classes
The library provides error classes for various HTTP status codes:
import {
AppError, // Base error class
ValidationError, // 400 - Validation failed
AuthenticationError, // 401 - Authentication failed
AuthorizationError, // 403 - Insufficient permissions
NotFoundError, // 404 - Resource not found
ConflictError, // 409 - Resource conflict
BusinessLogicError, // 422 - Business logic error
RateLimitError, // 429 - Rate limit exceeded
DatabaseError, // 500 - Database error
ExternalAPIError, // 502 - External API error
} from "postgre-pagination";
// Usage examples
throw new ValidationError("Invalid email format", {
field: "email",
value: req.body.email,
});
throw new NotFoundError("User not found", {
userId: req.params.id,
});
throw new ConflictError("Email already exists", {
email: req.body.email,
});Error Response Format:
{
"ok": false,
"statusCode": 404,
"errorCode": "NOT_FOUND_ERROR",
"message": "User not found",
"details": {
"userId": "123"
}
}Global Error Handler
Handle errors across your entire application.
import express from "express";
import {
globalErrorHandler,
initializeErrorHandling,
} from "postgre-pagination";
const app = express();
// Register routes
app.use("/api", apiRouter);
// Global error handler (must be registered after routes)
app.use(
globalErrorHandler({
showStackTrace: process.env.NODE_ENV === "development",
logger: (error, req) => {
// Custom logging logic
},
})
);
// Or initialize the complete error handling system
app.use(
initializeErrorHandling({
notifyError: async (error, req) => {
// Notify critical errors via Slack/Email
},
})
);4. Response Utilities
Respond Class
Provides consistent API response format.
import { Respond } from "postgre-pagination";
// 200 OK - Success
Respond.ok(res, data, "Successfully retrieved");
// 201 Created - Resource created
Respond.created(res, newUser, "User created");
// 400 Bad Request - Invalid request
Respond.badRequest(res, "Invalid request");
// 401 Unauthorized - Authentication required
Respond.unauthorized(res, "Authentication required");
// 403 Forbidden - Insufficient permissions
Respond.forbidden(res, "Access denied");
// 404 Not Found - Resource not found
Respond.notFound(res, "Resource not found");
// 429 Too Many Requests - Rate limit exceeded
Respond.tooManyRequests(res, "Too many requests");
// 500 Internal Server Error - Server error
Respond.internalError(res, "Internal server error");
// 502 Bad Gateway - External service error
Respond.badGateway(res, "External service error");Response Format:
{
"ok": true,
"statusCode": 200,
"message": "Successfully retrieved",
"data": {
"users": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 100
}
}
}5. Rate Limiting
routeLimit(maxRequests, windowMs?, keyGenerator?)
Controls the number of requests to protect your API from abuse.
Parameters:
maxRequests: Maximum number of requests allowedwindowMs(optional): Time window in milliseconds (default: 60000 = 1 minute)keyGenerator(optional): Function to generate unique keys (default: IP + path)
Usage:
import express from "express";
import {
routeLimit,
globalRateLimit,
startAutoCleanup,
} from "postgre-pagination";
const app = express();
// Start automatic cleanup (recommended)
const cleanupTimer = startAutoCleanup();
// Global rate limit: 200 requests per hour
app.use(globalRateLimit(200, 60 * 60 * 1000));
// Specific route: 10 requests per minute
app.get("/api/limited", routeLimit(10, 60 * 1000), (req, res) => {
res.json({ success: true });
});
// Sensitive endpoint: 3 requests per minute
app.post("/api/auth/login", routeLimit(3, 60 * 1000), (req, res) => {
res.json({ token: "xxx" });
});
// Cleanup on shutdown
process.on("SIGTERM", () => {
clearInterval(cleanupTimer);
});Advanced Usage:
import { routeLimit, ipRateLimit } from "postgre-pagination";
// User-based rate limiting
app.get(
"/api/user/profile",
routeLimit(
100,
60 * 60 * 1000,
(req) => `user:${req.user?.id || "anonymous"}`
),
handler
);
// API key-based rate limiting
app.get(
"/api/external",
routeLimit(
500,
60 * 60 * 1000,
(req) => `apikey:${req.headers["x-api-key"] || "none"}`
),
handler
);
// IP-based rate limiting
app.use(ipRateLimit(100, 30 * 60 * 1000)); // 100 requests per 30 minutesResponse Headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1703145600000Rate Limit Exceeded Response (429):
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Please try again in 45 seconds.",
"retryAfter": 45,
"limit": 10,
"windowMs": 60000
}Monitoring:
import { getRateLimitStats, cleanupExpiredLimits } from "postgre-pagination";
// Get statistics
app.get("/admin/rate-limit-stats", (req, res) => {
const stats = getRateLimitStats();
res.json(stats);
// { totalKeys: 150, activeKeys: 120, expiredKeys: 30 }
});
// Manual cleanup
app.post("/admin/cleanup", (req, res) => {
cleanupExpiredLimits();
res.json({ message: "Cleanup completed" });
});Key Features:
- ✅ No User Tables Required - Uses PostgreSQL system views (
pg_stat_activity) - ✅ Memory-based - Fast performance
- ✅ Auto Cleanup - Automatic expired data cleanup with
startAutoCleanup() - ✅ Standard Headers - Supports
X-RateLimit-*andRetry-Afterheaders - ✅ Error Safe - Service continues even if rate limiting fails
Important Notes:
- Memory-based: Counters reset on server restart
- Single Server: Not shared across multiple server instances
- Auto Cleanup Recommended: Use
startAutoCleanup()for long-running applications
🎯 Real-World Examples
Complete CRUD API Implementation
import express, { Request, Response } from "express";
import {
paginationMiddleware,
searchMiddleware,
catchAsyncErrors,
Respond,
NotFoundError,
ValidationError,
ConflictError,
} from "postgre-pagination";
const router = express.Router();
interface User {
id: number;
email: string;
name: string;
role: string;
}
// List with pagination and search
router.get(
"/users",
paginationMiddleware({ limit: 20 }),
searchMiddleware<User>(["email", "name", "role"], ["email", "name"]),
catchAsyncErrors(async (req: Request, res: Response) => {
const { page, limit } = req.pagination;
const searchOptions = req.search;
const result = await userService.findAll({
pagination: { page, limit },
search: searchOptions,
});
return Respond.ok(res, result);
})
);
// Get single item
router.get(
"/users/:id",
catchAsyncErrors(async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User not found", {
userId: req.params.id,
});
}
return Respond.ok(res, user);
})
);
// Create
router.post(
"/users",
catchAsyncErrors(async (req: Request, res: Response) => {
const { email, name, password } = req.body;
// Validation
if (!email || !name || !password) {
throw new ValidationError("Required fields are missing", {
required: ["email", "name", "password"],
provided: Object.keys(req.body),
});
}
// Check for duplicates
const existing = await userService.findByEmail(email);
if (existing) {
throw new ConflictError("Email already exists", {
email,
});
}
const user = await userService.create({ email, name, password });
return Respond.created(res, user, "User created successfully");
})
);
// Update
router.patch(
"/users/:id",
catchAsyncErrors(async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User not found");
}
const updated = await userService.update(req.params.id, req.body);
return Respond.ok(res, updated, "User updated successfully");
})
);
// Delete
router.delete(
"/users/:id",
catchAsyncErrors(async (req: Request, res: Response) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User not found");
}
await userService.delete(req.params.id);
return Respond.ok(res, null, "User deleted successfully");
})
);
export default router;Usage with Authentication Middleware
import {
paginationMiddleware,
catchAsyncErrors,
Respond,
AuthenticationError,
AuthorizationError,
} from "postgre-pagination";
// Authentication middleware
const authenticate = catchAsyncErrors(async (req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
throw new AuthenticationError("Authentication token required");
}
const user = await authService.verifyToken(token);
if (!user) {
throw new AuthenticationError("Invalid token");
}
req.user = user;
next();
});
// Authorization middleware
const authorize = (...roles: string[]) => {
return catchAsyncErrors(async (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
throw new AuthorizationError("Insufficient permissions", {
required: roles,
current: req.user?.role,
});
}
next();
});
};
// Usage example
router.get(
"/admin/users",
authenticate,
authorize("admin", "moderator"),
paginationMiddleware(),
catchAsyncErrors(async (req, res) => {
const { page, limit } = req.pagination;
const users = await userService.findAll({ page, limit });
return Respond.ok(res, users);
})
);🔧 Configuration
Customizing Pagination Defaults
// src/configs/Pagination.ts
export const PAGINATION_DEFAULTS = {
page: 1,
limit: 10,
minLimit: 1,
maxLimit: 100,
};
export const PAGINATION_ERRORS = {
INVALID_PAGE: "Invalid page number. Page must be a positive integer.",
INVALID_LIMIT: "Invalid limit. Limit must be between 1 and 100.",
PAGE_NOT_EXISTS: (page: number, totalPages: number) =>
`Page ${page} does not exist. Total pages: ${totalPages}`,
};🌟 Benefits
Without This Library
// Repetitive and messy code
router.get("/posts", async (req, res) => {
try {
// Pagination validation
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
if (page < 1) {
return res.status(400).json({ message: "Invalid page number" });
}
if (limit < 1 || limit > 100) {
return res.status(400).json({ message: "Invalid limit value" });
}
// Search validation
const search = req.query.search as string;
const searchFields = req.query.searchFields as string;
// ... more validation code
const result = await postsService.read({ page, limit });
res.status(200).json({
ok: true,
data: result,
});
} catch (error) {
console.error(error);
res.status(500).json({ message: "Internal server error" });
}
});With This Library
// Clean and readable code
router.get(
"/posts",
paginationMiddleware(),
catchAsyncErrors(async (req, res) => {
const { page, limit } = req.pagination;
const result = await postsService.read({ page, limit });
return Respond.ok(res, result);
})
);Code reduction: ~70% 🎉
📝 TypeScript Type Definitions
// Express Request extensions
declare global {
namespace Express {
interface Request {
pagination?: {
page: number;
limit: number;
};
search?: {
searchTerm?: string;
searchFields?: string[];
searchMode?: "like" | "ilike" | "fts";
caseSensitive?: boolean;
multiWord?: boolean;
};
user?: any;
}
}
}🤝 Contributing
Contributions are always welcome! Feel free to open issues or submit Pull Requests.
📄 License
MIT © suhyun751207
🔗 Links
💡 Why This Library?
When developing Express routes, you often encounter repetitive patterns:
- ✅ Extracting and validating pagination parameters
- ✅ Processing search parameters
- ✅ Error handling and logging
- ✅ Consistent response formats
This library automates these repetitive tasks to:
- 🚀 Increase Development Speed - Eliminate boilerplate code
- 🐛 Reduce Bugs - Reuse validated code
- 📖 Improve Readability - Focus on business logic
- 🔒 Type Safety - Full TypeScript support
Express API development made easier! 🎉
