GraphQL
Filtering, Sorting, and Searching in GraphQL
Advanced Query Capabilities
Implementing flexible filtering, sorting, and searching capabilities makes your GraphQL API more powerful and user-friendly. These features allow clients to query exactly the data they need.
Basic Filtering with Arguments
Start with simple filter arguments in your schema:
type Query {
posts(
category: String
status: PostStatus
authorId: ID
featured: Boolean
): [Post!]!
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Post {
id: ID!
title: String!
content: String!
category: String!
status: PostStatus!
featured: Boolean!
authorId: ID!
createdAt: DateTime!
}
Basic Filter Resolver
const resolvers = {
Query: {
posts: async (_, { category, status, authorId, featured }) => {
// Build filter object
const filter = {};
if (category) filter.category = category;
if (status) filter.status = status;
if (authorId) filter.authorId = authorId;
if (featured !== undefined) filter.featured = featured;
return await Post.find(filter).sort({ createdAt: -1 });
}
}
};
// Example queries:
// query {
// posts(category: "Technology", status: PUBLISHED) {
// id
// title
// }
// }
//
// query {
// posts(featured: true) {
// id
// title
// }
// }
Advanced Filter Input Type
Use input types for more complex filtering:
type Query {
posts(filter: PostFilter, sort: PostSort): [Post!]!
}
input PostFilter {
category: String
status: PostStatus
authorId: ID
featured: Boolean
search: String
createdAfter: DateTime
createdBefore: DateTime
tags: [String!]
}
input PostSort {
field: PostSortField!
order: SortOrder = DESC
}
enum PostSortField {
CREATED_AT
UPDATED_AT
TITLE
VIEWS
}
enum SortOrder {
ASC
DESC
}
Advanced Filter Resolver
const resolvers = {
Query: {
posts: async (_, { filter = {}, sort = {} }) => {
// Build MongoDB query
const query = {};
// Simple equality filters
if (filter.category) query.category = filter.category;
if (filter.status) query.status = filter.status;
if (filter.authorId) query.authorId = filter.authorId;
if (filter.featured !== undefined) query.featured = filter.featured;
// Array filters
if (filter.tags && filter.tags.length > 0) {
query.tags = { $in: filter.tags };
}
// Date range filters
if (filter.createdAfter || filter.createdBefore) {
query.createdAt = {};
if (filter.createdAfter) {
query.createdAt.$gte = new Date(filter.createdAfter);
}
if (filter.createdBefore) {
query.createdAt.$lte = new Date(filter.createdBefore);
}
}
// Text search
if (filter.search) {
query.$or = [
{ title: { $regex: filter.search, $options: 'i' } },
{ content: { $regex: filter.search, $options: 'i' } }
];
}
// Build sort object
const sortField = sort.field || 'CREATED_AT';
const sortOrder = sort.order === 'ASC' ? 1 : -1;
const sortMap = {
CREATED_AT: 'createdAt',
UPDATED_AT: 'updatedAt',
TITLE: 'title',
VIEWS: 'views'
};
const sortObj = { [sortMap[sortField]]: sortOrder };
return await Post.find(query).sort(sortObj);
}
}
};
Tip: Use input types for complex filters instead of many individual arguments. This keeps your schema clean and extensible.
Where Clause Pattern
Implement flexible where clauses similar to ORMs:
type Query {
posts(where: PostWhereInput, orderBy: PostOrderByInput): [Post!]!
}
input PostWhereInput {
id: IDFilter
title: StringFilter
content: StringFilter
category: StringFilter
status: PostStatus
featured: Boolean
views: IntFilter
createdAt: DateTimeFilter
author: UserWhereInput
AND: [PostWhereInput!]
OR: [PostWhereInput!]
NOT: PostWhereInput
}
input StringFilter {
equals: String
not: String
in: [String!]
notIn: [String!]
contains: String
startsWith: String
endsWith: String
}
input IntFilter {
equals: Int
not: Int
in: [Int!]
notIn: [Int!]
lt: Int
lte: Int
gt: Int
gte: Int
}
input DateTimeFilter {
equals: DateTime
not: DateTime
in: [DateTime!]
notIn: [DateTime!]
lt: DateTime
lte: DateTime
gt: DateTime
gte: DateTime
}
input PostOrderByInput {
createdAt: SortOrder
updatedAt: SortOrder
title: SortOrder
views: SortOrder
}
Where Clause Resolver
function buildWhereClause(where) {
if (!where) return {};
const query = {};
// Handle logical operators
if (where.AND) {
query.$and = where.AND.map(buildWhereClause);
}
if (where.OR) {
query.$or = where.OR.map(buildWhereClause);
}
if (where.NOT) {
query.$not = buildWhereClause(where.NOT);
}
// Handle field filters
Object.keys(where).forEach(key => {
if (['AND', 'OR', 'NOT'].includes(key)) return;
const filter = where[key];
// Simple equality
if (typeof filter !== 'object' || filter === null) {
query[key] = filter;
return;
}
// StringFilter
if (filter.equals) query[key] = filter.equals;
if (filter.not) query[key] = { $ne: filter.not };
if (filter.in) query[key] = { $in: filter.in };
if (filter.notIn) query[key] = { $nin: filter.notIn };
if (filter.contains) {
query[key] = { $regex: filter.contains, $options: 'i' };
}
if (filter.startsWith) {
query[key] = { $regex: `^${filter.startsWith}`, $options: 'i' };
}
if (filter.endsWith) {
query[key] = { $regex: `${filter.endsWith}$`, $options: 'i' };
}
// IntFilter / DateTimeFilter
if (filter.lt) query[key] = { ...query[key], $lt: filter.lt };
if (filter.lte) query[key] = { ...query[key], $lte: filter.lte };
if (filter.gt) query[key] = { ...query[key], $gt: filter.gt };
if (filter.gte) query[key] = { ...query[key], $gte: filter.gte };
});
return query;
}
const resolvers = {
Query: {
posts: async (_, { where, orderBy }) => {
const query = buildWhereClause(where);
let dbQuery = Post.find(query);
if (orderBy) {
const sort = {};
Object.keys(orderBy).forEach(key => {
sort[key] = orderBy[key] === 'ASC' ? 1 : -1;
});
dbQuery = dbQuery.sort(sort);
}
return await dbQuery;
}
}
};
Note: Complex where clauses provide Prisma-like filtering capabilities, making your API more powerful and flexible.
Full-Text Search
Implement full-text search with MongoDB text indexes:
// Create text index in MongoDB
// db.posts.createIndex({ title: "text", content: "text" })
type Query {
searchPosts(query: String!, limit: Int = 20): [PostSearchResult!]!
}
type PostSearchResult {
post: Post!
score: Float!
}
const resolvers = {
Query: {
searchPosts: async (_, { query, limit }) => {
const results = await Post.find(
{ $text: { $search: query } },
{ score: { $meta: 'textScore' } }
)
.sort({ score: { $meta: 'textScore' } })
.limit(limit);
return results.map(post => ({
post,
score: post.score
}));
}
}
};
// Example query:
// query {
// searchPosts(query: "GraphQL tutorial", limit: 10) {
// post {
// id
// title
// }
// score
// }
// }
Combining Filters, Sort, and Pagination
type Query {
posts(
filter: PostFilter
sort: PostSort
pagination: PaginationInput
): PostConnection!
}
input PaginationInput {
first: Int
after: String
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
const resolvers = {
Query: {
posts: async (_, { filter, sort, pagination }) => {
// Build query from filter
const query = buildFilterQuery(filter);
// Build sort object
const sortObj = buildSortObject(sort);
// Apply pagination
let dbQuery = Post.find(query).sort(sortObj);
if (pagination?.after) {
const cursor = decodeCursor(pagination.after);
query._id = { $lt: cursor };
}
const limit = pagination?.first || 10;
const posts = await dbQuery.limit(limit + 1);
// Check for more results
const hasMore = posts.length > limit;
const nodes = hasMore ? posts.slice(0, limit) : posts;
// Get total count
const totalCount = await Post.countDocuments(query);
return {
edges: nodes.map(post => ({
node: post,
cursor: encodeCursor(post._id)
})),
pageInfo: {
hasNextPage: hasMore,
hasPreviousPage: !!pagination?.after,
startCursor: nodes[0] ? encodeCursor(nodes[0]._id) : null,
endCursor: nodes[nodes.length - 1]
? encodeCursor(nodes[nodes.length - 1]._id)
: null
},
totalCount
};
}
}
};
// Example query combining everything:
// query {
// posts(
// filter: {
// category: "Technology"
// status: PUBLISHED
// createdAfter: "2026-01-01"
// search: "GraphQL"
// }
// sort: { field: VIEWS, order: DESC }
// pagination: { first: 20 }
// ) {
// edges {
// node {
// id
// title
// views
// }
// }
// pageInfo {
// hasNextPage
// endCursor
// }
// totalCount
// }
// }
Warning: Complex filters can be expensive. Always add database indexes for frequently filtered fields and monitor query performance.
Exercise:
- Implement a
usersquery with filters for role, status, registration date range, and search - Create a where clause system that supports AND, OR, and NOT logical operators
- Add full-text search with relevance scoring for blog posts
- Build a products query that supports filtering by price range, category, rating, and availability
- Combine filtering, sorting, and cursor-based pagination in a single query