Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
16 Oct 2025 · 6 min read ·Article 108 / 125
Go

108 Handling Automatic and Manual Resolvers

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

js
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

ApproachAutomatic ResolverManual Resolver
DefinitionResolver generated automatically by the systemResolver written by the developer themselves
ExampleORM auto-mapping, GraphQL codegenCustom function, personalized query
ProsLittle boilerplate, fast, consistentFlexible, can be optimized for specific needs
ConsLess control, sometimes overfetching/underfetchingMore code, bug-prone, heavy maintenance
Ideal CaseStandard CRUD, simple schemaComplex 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:

MERMAID
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)

ts
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:

js
 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}
Here, we have full control over how user data is fetched, processed, and returned to the client, and we can also add validation and auditing.


When to Use Automatic, When to Use Manual?

Below are the considerations, which I’ve summarized in the following table:

ScenarioAutomaticManual
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

ts
1const resolvers = {
2  Query: {
3    usersWithOrders: (_, __, { db }) =>
4      db.user.findMany({
5        include: { orders: true }
6      })
7  }
8}
Average query time: 50 ms
Code: 3 lines

Benchmark: Manual Resolver

ts
 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}
Average query time: 120 ms
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.

js
 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

  1. Start with Automatic: For prototyping/MVP.
  2. Refactor to Manual When Needed: For transformation, optimization, auditing, security.
  3. Use Middleware: For logging/validation without the hassle of modifying every manual resolver one by one.
  4. Automate the Boring Stuff: Partial automation — use a code generator.
  5. Test Resolvers Thoroughly: Because resolver bugs directly impact users.
  6. 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

Related Articles

💬 Comments