108 Handling Automatic and Manual Resolvers
108 Handling Automatic and Manual Resolvers: A Complete Guide for Engineers
Resolvers are a key component in many modern software systems, from dependency management in web frameworks to parsing complex queries in GraphQL. However, we often find ourselves at a crossroads with two main paths when building resolvers: automatic or manual. Deciding when and how to use each approach is an art — and a science — in its own right.
In this article, I’ll walk through 108 ways (or at least dig down to the finest details!) to handle automatic and manual resolvers, covering the right mindset, design patterns, code examples, simulations, and practical tips for production development.
Getting to Know Resolvers
In general terms, a resolver is a function or piece of logic responsible for locating, fetching, and assembling the final data requested by the client. In a modern context, you’ll find them in:
- ORMs (Object-Relational Mapping), such as Sequelize or Prisma
- API frameworks (Express.js, NestJS, Laravel, and so on)
- The GraphQL API layer
For example, in GraphQL:
1const resolvers = {
2 Query: {
3 user: (_, { id }, { db }) => db.users.findById(id)
4 }
5}The resolver above looks up a user by id from the database context.
Automatic vs. Manual Resolvers: A Quick Definition
| Approach | Automatic Resolver | Manual Resolver |
|---|---|---|
| Definition | Resolver generated automatically by the system | Resolver written by the developer themselves |
| Example | ORM auto-mapping, GraphQL codegen | Custom function, personalized query |
| Pros | Little boilerplate, fast, consistent | Flexible, can be optimized for specific needs |
| Cons | Less control, sometimes overfetching/underfetching | More code, bug-prone, heavy maintenance |
| Ideal Case | Standard CRUD, simple schema | Complex business logic, dynamic queries |
How Automatic Resolvers Work
Imagine you’re building a REST API with an ORM like Sequelize, or a GraphQL API with Prisma. These ORMs provide automatic resolvers that map table definitions onto API endpoints/fields.
The flow looks roughly like this:
flowchart TD
A[Client Request Field] --> B[Endpoint/Query Received]
B --> C[Schema/Syntax Analyzer]
C -- auto map --> D[ORM/Database Layer]
D --> E[Auto-generated Resolver]
E --> F[Fetch/Transform Data]
F --> G[Return Response]
This process saves a lot of development time by eliminating the need to write resolvers one by one.
Automation Example with Prisma (TypeScript + GraphQL)
1import { PrismaClient } from '@prisma/client';
2const prisma = new PrismaClient();
3
4const resolvers = {
5 Query: {
6 users: () => prisma.user.findMany(), // Prisma generates the auto-mapping
7 posts: () => prisma.post.findMany(),
8 }
9}Prisma runs the query automatically; you simply call the findMany() function.
Manual Resolvers: When Automation Isn’t Enough
There are cases where automatic resolvers just aren’t flexible enough. For example:
- You need a highly custom relational join
- You need extra business logic during the fetching process
- You need special validation, transformation, or logging
Example of a Manual Resolver in GraphQL:
1const resolvers = {
2 Query: {
3 userProfile: async (_, { userId }, { db }) => {
4 const user = await db.users.findById(userId);
5 if (!user) throw new Error("User not found");
6 // Extra business logic: track access
7 await db.audit.logAccess(userId, 'profile_view');
8 return {
9 ...user,
10 isPremium: user.subscription.status === "active",
11 // other transformations
12 }
13 }
14 }
15}When to Use Automatic, When to Use Manual?
Below are the considerations, which I’ve summarized in the following table:
| Scenario | Automatic | Manual |
|---|---|---|
| Simple CRUD | ✔️ | |
| Query optimization (select field) | ✔️ | |
| Complex data transformation | ✔️ | |
| Unique business logic | ✔️ | |
| Quick MVP setup | ✔️ | |
| Dynamic/evolving schema | ✔️ | |
| Audit, Logging, Validation | ✔️ |
Simulation: Performance Comparison
For a case study, let’s assume we want to fetch user data along with their orders, using both an automatic resolver (for example with an ORM) and a manual resolver.
Benchmark: Automatic Resolver
1const resolvers = {
2 Query: {
3 usersWithOrders: (_, __, { db }) =>
4 db.user.findMany({
5 include: { orders: true }
6 })
7 }
8}Code: 3 lines
Benchmark: Manual Resolver
1const resolvers = {
2 Query: {
3 usersWithOrders: async (_, __, { db }) => {
4 const users = await db.user.findMany();
5 return Promise.all(users.map(async (user) => ({
6 ...user,
7 orders: await db.order.findMany({ where: { userId: user.id } })
8 })));
9 }
10 }
11}Code: 8 lines, more flexible (you can add per-user business logic)
Insights
- Automatic is fast, but you can’t filter/transform per user.
- Manual is slower (the N+1 problem), but it can be customized.
The solution? Use a DataLoader or batched queries in the manual resolver to keep performance optimal.
Combining Both: Hybrid Resolvers
Combine the two for both efficiency and flexibility. This kind of hybrid technique is commonly found in large companies.
1const resolvers = {
2 Query: {
3 users: async (_, __, { db }) => {
4 const users = await db.user.findMany();
5 return users.map(user => ({
6 ...user,
7 premiumStatus: computePremiumStatus(user)
8 }))
9 }
10 },
11 User: {
12 orders: (parent, _, { db }) => db.order.findMany({ where: { userId: parent.id } })
13 }
14}- The Query part stays efficient (automatic),
- Specific fields (orders, premiumStatus) can be manually customized.
Engineering Best Practices
- Start with Automatic: For prototyping/MVP.
- Refactor to Manual When Needed: For transformation, optimization, auditing, security.
- Use Middleware: For logging/validation without the hassle of modifying every manual resolver one by one.
- Automate the Boring Stuff: Partial automation — use a code generator.
- Test Resolvers Thoroughly: Because resolver bugs directly impact users.
- Monitor Overfetching/Underfetching: Automatic resolvers are prone to fetching too much/too little data.
Conclusion
Handling resolvers — whether automatic or manual — is all about the trade-off between development speed and depth of control. Automate when you can. Go manual when you must. There’s no single silver bullet. Good engineering means knowing when to optimize and when automation is good enough.
Always measure your project’s needs, and don’t hesitate to combine both in a modern architecture. My experience at several national-scale companies has shown that a hybrid style is often the best solution for real-world production problems.
Feel free to share your experiences building or using resolvers — automatic, manual, or a combination — in the comments!
References:
Engineering regards,
Ariv — Senior Software Engineer