Back to Blog
Lesson 35 of the GraphQL: Your First GraphQL Schema & Server course
API ArchitectureAugust 22, 20264 min read

Validating Inputs: Ensuring Data Integrity in GraphQL Mutations

Learn how to implement server-side validation in GraphQL. Discover how to move beyond basic schema types to enforce business logic and prevent bad data.

GraphQLAPI ArchitectureSecurityValidationBackend DevelopmentNode.js
Wooden blocks aligned to spell 'CHECK' with a checkmark symbol on a neutral background.

Previously in this course, we covered Input Types: Structuring GraphQL Mutations for Clean Code to keep our arguments organized. While that taught us how to structure data, it didn't teach us how to ensure that the data is correct. In this lesson, we move from structural requirements to business requirements: validating the actual values to keep our application's state clean and secure.

Why Schema-Level Validation Isn't Enough

GraphQL provides built-in validation through its type system. If you define an argument as Int!, GraphQL automatically rejects any request that provides a String or null. This is the first line of defense for Validation.

However, the schema cannot understand your business rules. It doesn't know that a username must be at least 3 characters long, or that a price cannot be negative. If you rely solely on schema types, you will end up with "technically correct" but "logically invalid" data in your database.

Implementing Business Logic Validation

To prevent invalid data entries, we must perform manual checks inside our resolver functions. The most effective place for this is at the very beginning of the mutation resolver.

Let’s extend our running project. Suppose we are adding a createProduct mutation. We need to ensure the price is positive and the name isn't just whitespace.

JAVASCRIPT
// Resolvers.js
const resolvers = {
  Mutation: {
    createProduct: (_, { input }) => {
      // 1. Check for valid length
      if (input.name.trim().length < 3) {
        throw new Error("Product name must be at least 3 characters long.");
      }

      // 2. Check for logical constraints
      if (input.price < 0) {
        throw new Error("Price cannot be negative.");
      }

      // If checks pass, proceed to save logic
      return db.products.create(input);
    }
  }
};

The Strategy: Fail Fast

As shown above, we use the "fail fast" pattern. By throwing an error at the top of the function, we prevent the execution of any downstream code (like database calls), which is critical for security. If an attacker tries to inject a massive price value or invalid string, the server rejects the request before it reaches your persistence layer.

This approach is highly similar to the concepts discussed in Implementing Input Validation: Securing Your Node.js API, where we emphasize validating inputs before they reach critical business logic.

Hands-on Exercise

  1. Open your current Mutation resolver for your project.
  2. Identify one input field that could contain "garbage" data (e.g., an empty string, a number out of range, or an invalid format).
  3. Add a conditional check at the start of that resolver.
  4. If the condition fails, use throw new Error(...) to return a descriptive message to the client.
  5. Test it in Apollo Sandbox to see the error returned in the errors array of the JSON response.

Common Pitfalls

  • Logic in the wrong layer: Don't put business validation in the database layer. Always validate at the entry point of your API (the resolver).
  • Vague error messages: Avoid "Invalid input." Instead, be specific: "Product name must be at least 3 characters." This helps the frontend developer fix the issue quickly.
  • Assuming trust: Never assume that just because a request reached your server, the data is safe. Even if your internal frontend is perfect, anyone can use a tool like Postman or curl to send malformed data to your endpoint.

FAQ

Q: Should I use a library like Joi or Yup for validation? A: Yes, as your schema grows, manual if statements become unmanageable. Libraries like Joi are excellent for defining schema-based validation rules that you can reuse across multiple mutations.

Q: Does validation happen before or after the resolver runs? A: Basic type validation (Int vs String) happens before the resolver. Business logic validation (price > 0) happens inside the resolver.

Q: Can I use custom scalars for validation? A: Yes! If you find yourself checking for an "Email" or "Date" format in every resolver, you should implement a custom scalar (which we will cover in a later lesson) to handle that validation automatically at the schema level.

Recap

We’ve learned that while GraphQL handles type safety, we are responsible for business logic. By throwing errors early in our resolvers, we keep our data clean and protect our system from invalid state. We've moved our project forward by adding a guard against bad inputs, ensuring the reliability of our API.

Up next: We will learn how to connect our server to real-world data by reading from a JSON file, moving us away from hardcoded objects.

Similar Posts