GraphQL

Filtering, Sorting, and Searching in GraphQL

19 min Lesson 14 of 35

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:
  1. Implement a users query with filters for role, status, registration date range, and search
  2. Create a where clause system that supports AND, OR, and NOT logical operators
  3. Add full-text search with relevance scoring for blog posts
  4. Build a products query that supports filtering by price range, category, rating, and availability
  5. Combine filtering, sorting, and cursor-based pagination in a single query