33 Writing Query Resolvers from the Database
33 Writing Query Resolvers from the Database
In the modern era of application development, backend architecture increasingly caters to the client’s need for efficient and flexible data access. One of the most up-to-date approaches is using GraphQL as the query language for an API. On the backend side, we are often faced with the need to write resolvers so that data in the database can be queried easily by clients. This article will break down how to write 33 query resolvers from the database, using a systematic and scalable approach.
We will move from the fundamental concepts, to code examples, flow simulations, and even a simple benchmark. Assumption: the backend uses Node.js with Apollo Server and the Sequelize ORM as the data layer to a relational database (PostgreSQL).
What Is a Query Resolver?
A GraphQL resolver is a function that handles fetching data from a source (usually a database) based on the client’s query request. A resolver can handle fetching a single row, multiple rows, filtering, sorting, and even data relationships.
Every field in a GraphQL schema that needs data from the database usually has its own resolver.
Designing the Model and Database Schema
Let’s imagine a simple database for an online store application:
| Table | Fields |
|---|---|
| User | id, name, email |
| Product | id, name, price, stock |
| Order | id, userId, productId, quantity, createdAt |
Let’s assume there are 33 query scenarios we want to support, such as:
- Query user by id
- Query all users
- Query user by email
- Query product by id
- Query products by name
- Query all products sorted by price, and so on.
Query Resolver Flow Diagram
To understand the process, here is the query resolver flow written in mermaid code:
flowchart TD
A[Client queries GraphQL] --> B[GraphQL Server menerima query]
B --> C[Parsing & Validating Query]
C --> D[Menemukan Resolver]
D --> E[Resolver Fetch ke DB lewat ORM]
E --> F[ORM Kirim Data ke Resolver]
F --> G[Resolver Format Data]
G --> H[Balas ke Client]
Example GraphQL Schema
1type Query {
2 userById(id: ID!): User
3 users: [User]
4 userByEmail(email: String!): User
5 productById(id: ID!): Product
6 productsByName(name: String!): [Product]
7 allProducts(sortByPrice: Boolean): [Product]
8 # ...up to 33 query endpoints
9}
10
11type User {
12 id: ID
13 name: String
14 email: String
15 orders: [Order]
16}
17
18type Product {
19 id: ID
20 name: String
21 price: Float
22 stock: Int
23}
24
25type Order {
26 id: ID
27 user: User
28 product: Product
29 quantity: Int
30 createdAt: String
31}Resolver File Structure & Best Practices
To keep things maintainable, it’s best to split resolvers by resource/domain:
1resolvers/
2 ├─ index.js
3 ├─ userResolver.js
4 ├─ productResolver.js
5 └─ orderResolver.jsAnd each file has its own resolutions.
Implementing Resolvers with Sequelize
Let’s jump straight into an implementation example. For instance: userResolver.js
1// userResolver.js
2const { User, Order } = require('../models');
3
4const userResolver = {
5 Query: {
6 userById: async (_, { id }) => {
7 return await User.findByPk(id);
8 },
9 users: async () => {
10 return await User.findAll();
11 },
12 userByEmail: async (_, { email }) => {
13 return await User.findOne({ where: { email } });
14 }
15 },
16 User: {
17 orders: async (parent) => {
18 return await Order.findAll({ where: { userId: parent.id } });
19 }
20 }
21};
22
23module.exports = userResolver;A few notes:
- Each function returns a promise, ready for async-await.
- The use of parent in the field resolver maintains the relationship.
Adding Resolvers for 33 Queries
To manage 33 resolvers, use a generator pattern or a template. For example, in productResolver.js:
1const { Product } = require('../models');
2
3const productResolver = {
4 Query: {
5 productById: async (_, { id }) => await Product.findByPk(id),
6 productsByName: async (_, { name }) =>
7 await Product.findAll({ where: { name } }),
8 allProducts: async (_, { sortByPrice }) =>
9 await Product.findAll({
10 order: sortByPrice ? [['price', 'ASC']] : undefined
11 }),
12 // ...Add 30 more as per requirements
13 }
14};
15
16module.exports = productResolver;For scalability and DRY (Don’t Repeat Yourself), create a utility to automate several resolvers:
1function createSimpleFindQuery(model, by) {
2 return async (_, args) => await model.findOne({ where: { [by]: args[by] } });
3}Query Simulation: User with Orders
Suppose the user wants User data along with the list of Orders:
1query {
2 userById(id: "123") {
3 id
4 name
5 orders {
6 id
7 product {
8 name
9 price
10 }
11 quantity
12 createdAt
13 }
14 }
15}How does the resolver handle this?
- First it fetches the User via ID (userById)
- When the orders field is accessed, the
User.ordersresolver is triggered to fetch the orders - The product field within Order will trigger a separate resolver on the Order type
Simulated Output from the 33 Queries
Here is a miniature simulation of the output for 3 of the 33 queries:
| Query | Parameter | Example Output |
|---|---|---|
| userById | id: 123 | {“id”:123, “name”:“Rafi”, “email”:“rafi@foo.com ”} |
| userByEmail | email:foo@bar.com | {“id”:99, “name”:“Siti”, “email”:“foo@bar.com ”} |
| allProducts(sortByPrice: true) | sortByPrice: true | [{“id”:11,“name”:“T-shirt”,“price”:99.9,…}, …] |
Rules & Tips for Writing Many Resolvers
- DRY Principle: If there are similar queries, use a helper or generator.
- Validation: Validate parameters before hitting the DB to minimize errors and database risk.
- Batching & Caching: For nested resolvers, use DataLoader to prevent the N+1 query problem.
- Error Handling: Use try-catch in every resolver and keep the error format consistent.
- Documentation: Add JSDoc or at least a minimal comment for the endpoint/purpose of each resolver.
- Testing: Prepare dedicated unit/integration tests for every resolver.
Simple Benchmark
Suppose 33 queries are run in parallel by the client. Use Promise.all to test them simultaneously:
1const allQueries = [
2 gql1, gql2, /* ... up to gql33 */
3];
4
5Promise.all(allQueries.map(q => apolloServer.executeOperation({ query: q })))
6 .then(results => {
7 // Evaluate time taken, errors, etc.
8 });Conclusion
Writing 33 query resolvers is indeed challenging, especially when it comes to keeping the code scalable & maintainable. Focus on resolver structure, automate similar queries, and apply GraphQL best practices. Don’t forget testing and documentation, so that your team can maintain & evolve the codebase sustainably.
If you have any custom experience or tips for writing resolvers, feel free to share them in the comments! Happy coding 🚀