AI & Software Solution
API Design

REST API Query Parameters: Production Standards and Best Practices

GE

Guvantic Engineering

Guvantic Technical Publication

14 min
REST API Query Parameters: Production Standards and Best Practices
Executive Architectural Overview (AEO Direct Answer)

Complete guide to REST API query parameters: filtering, sorting, pagination, field selection, validation, and OpenAPI documentation. Build APIs that developers love to use.

Query parameters make REST APIs flexible and powerful. This guide covers the patterns used by Stripe, GitHub, and Shopify — with production-ready code examples.

The Problem: Inconsistent APIs

Developers hate inconsistent APIs. Different endpoints use different parameter names, unexpected pagination behavior, and unclear filtering options.

Architectural Topology Specification
Bad API Design vs Good API Design — Shows:
Bad: /users?page=1, /orders?limit=10 (different conventions)
Good: /users?page=1&limit=10, /orders?page=1&limit=10 (consistent)

Filtering Best Practices

Simple Equality Filters

typescriptProduction snippet
// GET /api/users?status=active&role=admin
app.get("/api/users", async (req, res) => {
  const { status, role } = req.query;
  const where: Prisma.UserWhereInput = {};

  if (status) where.status = status;
  if (role) where.role = role;

  const users = await prisma.user.findMany({ where });
  res.json(users);
});

Range Filters

typescriptProduction snippet
// GET /api/orders?created_after=2026-01-01&created_before=2026-12-31&amount_gte=100&amount_lte=500

function parseRangeFilters(query: any) {
  const filters: any = {};
  
  // Date ranges
  if (query.created_after || query.created_before) {
    filters.createdAt = {};
    if (query.created_after) filters.createdAt.gte = new Date(query.created_after);
    if (query.created_before) filters.createdAt.lte = new Date(query.created_before);
  }

  // Numeric ranges
  if (query.amount_gte || query.amount_lte) {
    filters.amount = {};
    if (query.amount_gte) filters.amount.gte = parseFloat(query.amount_gte);
    if (query.amount_lte) filters.amount.lte = parseFloat(query.amount_lte);
  }

  return filters;
}

Search (Full-Text)

typescriptProduction snippet
// GET /api/products?q=wireless+headphones
app.get("/api/products", async (req, res) => {
  const { q } = req.query;

  if (q) {
    const products = await prisma.product.findMany({
      where: {
        OR: [
          { name: { contains: q as string, mode: "insensitive" } },
          { description: { contains: q as string, mode: "insensitive" } },
        ],
      }
    });
    return res.json(products);
  }
});

Pagination Patterns

Offset Pagination

typescriptProduction snippet
// GET /api/users?page=2&limit=20
interface PaginatedResponse<T> {
  data: T[];
  meta: {
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  };
}

app.get("/api/users", async (req, res) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = Math.min(
    parseInt(req.query.limit as string) || 20,
    100 // Maximum limit
  );
  const skip = (page - 1) * limit;

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      skip,
      take: limit,
    }),
    prisma.user.count(),
  ]);

  res.json({
    data: users,
    meta: {
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    },
  });
});

Cursor Pagination (Recommended for large datasets)

typescriptProduction snippet
// GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
app.get("/api/users-cursor", async (req, res) => {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const cursor = req.query.cursor as string;

  let cursorObj: { id: number } | undefined;
  if (cursor) {
    cursorObj = JSON.parse(Buffer.from(cursor, "base64").toString());
  }

  const users = await prisma.user.findMany({
    take: limit + 1, // Fetch one extra to check if there are more
    cursor: cursorObj ? { id: cursorObj.id } : undefined,
    skip: cursorObj ? 1 : 0, // Skip the cursor item itself
    orderBy: { id: "asc" },
  });

  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;

  let nextCursor: string | undefined;
  if (hasMore && data.length > 0) {
    const lastItem = data[data.length - 1];
    const cursorObj = { id: lastItem.id };
    nextCursor = Buffer.from(JSON.stringify(cursorObj)).toString("base64");
  }

  res.json({
    data,
    meta: {
      limit,
      hasMore,
      nextCursor,
    },
  });
});

Sorting and Field Selection

Sorting

typescriptProduction snippet
// GET /api/users?sort=created_at:desc
// GET /api/users?sort=-created_at
// GET /api/users?sort=name:asc,created_at:desc

function parseSort(sort: string) {
  if (!sort) return {};
  
  const sortFields: Record<string, "asc" | "desc"> = {};
  const parts = sort.split(",");

  for (const part of parts) {
    const [field, direction] = part.split(":");
    if (direction === "desc" || part.startsWith("-")) {
      const cleanField = part.startsWith("-") ? part.slice(1) : field;
      sortFields[cleanField] = "desc";
    } else {
      sortFields[field] = "asc";
    }
  }

  return sortFields;
}

app.get("/api/users", async (req, res) => {
  const sort = parseSort(req.query.sort as string);
  const users = await prisma.user.findMany({ orderBy: sort });
  res.json(users);
});

Field Selection (Sparse Fieldsets)

typescriptProduction snippet
// GET /api/users?fields=id,name,email
app.get("/api/users", async (req, res) => {
  const fields = (req.query.fields as string)?.split(",") || [];
  
  // Validate fields against whitelist
  const allowedFields = ["id", "name", "email", "createdAt"];
  const selectedFields = fields.filter(f => allowedFields.includes(f));

  const users = await prisma.user.findMany({
    select: selectedFields.reduce((acc, field) => ({ ...acc, [field]: true }), {}),
  });

  res.json(users);
});

Validation and Security

Validation with Zod

typescriptProduction snippet
import { z } from "zod";

const QuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().positive().max(100).default(20),
  status: z.enum(["active", "inactive", "pending"]).optional(),
  sort: z.string().regex(/^[a-zA-Z_]+:(asc|desc)$/).optional(),
  fields: z.string().optional(),
});

app.get("/api/users", async (req, res) => {
  try {
    const validated = QuerySchema.parse(req.query);
    // Use validated parameters...
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({
        error: "Invalid query parameters",
        details: error.errors,
      });
    }
    throw error;
  }
});

Security Checklist:

✅Validate all query parameter types
✅Cap limit at a maximum (100-1000)
✅Whitelist sort fields
✅Validate enum values
✅Never pass query params directly to SQL
✅Log and monitor query parameter usage

OpenAPI Documentation

yamlProduction snippet
openapi: 3.0.0
paths:
  /api/users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive, pending]
        - name: sort
          in: query
          description: "Sort by field (e.g. created_at:desc)"
          schema:
            type: string
        - name: fields
          in: query
          description: "Comma-separated list of fields to return"
          schema:
            type: string
      responses:
        "200":
          description: Successful response
        "400":
          description: Invalid query parameters
Keywords:#REST API#query parameters#pagination#filtering#OpenAPI#API design
GE

Guvantic Engineering

Engineering & System Architecture Division

Engineering teams at Guvantic deliver mission-critical software, high-concurrency microservices, and international organic positioning strategies for fast-growing companies in USA, UK, and India.

Related Engineering Insights