npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

postgre-pagination

v1.0.3

Published

A lightweight PostgreSQL pagination for TypeScript

Readme

postgre-pagination

npm version License: MIT TypeScript

A powerful utility library for Express route development

Simplify pagination, search, and error handling to maximize Express API development productivity.

한국어 문서 (Korean Documentation)

✨ 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=yourpassword

Basic 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 values
    • page: 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=15

Validation Rules:

  • page: Integer greater than or equal to 1
  • limit: 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 fields
  • defaultFields (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=ilike

Query Parameters:

  • search: Search term
  • searchFields: Fields to search (comma-separated)
  • searchMode: Search mode
    • like: Case-sensitive search
    • ilike: 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 function
  • options (optional): Error handling options
    • logger: Custom logging function
    • errorTransformer: Error response transformation function
    • showStackTrace: 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 allowed
  • windowMs (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 minutes

Response Headers:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1703145600000

Rate 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-* and Retry-After headers
  • Error Safe - Service continues even if rate limiting fails

Important Notes:

  1. Memory-based: Counters reset on server restart
  2. Single Server: Not shared across multiple server instances
  3. 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! 🎉