Skip to content

GraphQL - Validation

Validation is crucial for ensuring data integrity when adding or modifying data via your GraphQL API. GraphQL offers several layers of validation:

  • Schema Validation: The server validates incoming queries against the schema before execution. It checks for correct syntax, existing types and fields, correct argument types, etc.
  • Type System Validation: GraphQL’s strong type system inherently enforces basic validation. You can mark fields and arguments as non-nullable or ensure they conform to specific scalar types (Int, String, Boolean, custom scalars).
  • Custom Input Validation: For more complex business rules (e.g., string length, email format, value ranges), you typically implement custom validation logic within your resolver functions or dedicated validation layers.

The simplest form of validation is ensuring a field or argument is never null. You achieve this by appending an exclamation mark (!) to the type definition in your schema.

type User {
id: ID! # ID must always be present and non-null
username: String! # Username must always be present and non-null
email: String # Email is optional (can be null)
age: Int # Age is optional (can be null)
}
input CreateUserInput {
username: String! # Required when creating a user
email: String # Optional when creating a user
password: String! # Required when creating a user
}
type Mutation {
createUser(input: CreateUserInput!): User! # Input object and returned User must be non-null
}

If a client tries to execute a query or mutation that violates these non-null constraints (e.g., omitting a required argument, or a resolver returns null for a non-nullable field), GraphQL will automatically generate an error before the resolver logic for dependent fields is even executed.

Implementing Custom Input Validation in Resolvers

Section titled “Implementing Custom Input Validation in Resolvers”

For validation beyond basic type checking and nullability (like format checks, length constraints, business rules), the logic resides within your resolver functions. It’s best practice to perform validation before attempting database operations or calling other services.

A common approach is to check input arguments at the beginning of your mutation resolver and throw an error if validation fails. GraphQL client libraries like Apollo Client can then catch these errors and display them to the user.

Let’s enhance a signUp mutation with custom validation for email format, password length, and username length.

Assume you have a basic Apollo Server setup similar to the one in the ‘Basic Server Example’ chapter. Ensure you have dependencies like @apollo/server, graphql, express.

Step 2: Define Schema (schema.graphql or in server.js)

Section titled “Step 2: Define Schema (schema.graphql or in server.js)”

Define the necessary types and the mutation:

#graphql
type Query {
# Placeholder query to make schema valid
_: Boolean
}
type User {
id: ID!
username: String!
email: String!
}
# Input type bundles arguments for the mutation
input SignUpInput {
username: String!
email: String!
password: String!
}
type Mutation {
signUp(input: SignUpInput!): User # Returns the created User on success
}

We use an input type SignUpInput to group the arguments for the signUp mutation, which is a recommended practice for mutations with multiple arguments.

Step 3: Create Resolvers with Validation (resolvers.js)

Section titled “Step 3: Create Resolvers with Validation (resolvers.js)”

Implement the signUp resolver with validation logic. We’ll use Apollo Server’s GraphQLError for throwing user-friendly errors.

import { GraphQLError } from 'graphql';
// Mock database interaction (replace with actual DB logic)
const mockUsers = [];
let nextUserId = 1;
const resolvers = {
Query: {
_: () => true, // Resolver for the placeholder query
},
Mutation: {
signUp: (_, { input }) => {
const { username, email, password } = input;
// --- Validation Logic ---
const errors = {};
// 1. Username validation
if (username.length < 3) {
errors.username = 'Username must be at least 3 characters long.';
}
if (username.length > 20) {
errors.username = 'Username cannot exceed 20 characters.';
}
// Check if username already exists (simple mock)
if (mockUsers.some(user => user.username === username)) {
errors.username = 'Username is already taken.';
}
// 2. Email validation (basic format check)
const emailRegex = /^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$/;
if (!emailRegex.test(String(email).toLowerCase())) {
errors.email = 'Please provide a valid email address.';
}
// Check if email already exists (simple mock)
if (mockUsers.some(user => user.email === email)) {
errors.email = 'Email is already registered.';
}
// 3. Password validation
if (password.length < 8) {
errors.password = 'Password must be at least 8 characters long.';
}
// --- End Validation Logic ---
// If any errors, throw a GraphQLError
if (Object.keys(errors).length > 0) {
throw new GraphQLError('Input validation failed', {
extensions: {
code: 'BAD_USER_INPUT',
// Pass validation errors details in extensions
validationErrors: errors
},
});
}
// --- If validation passes, proceed with user creation ---
console.log('Validation passed. Creating user...');
const newUser = {
id: String(nextUserId++),
username: username,
email: email,
// In a real app, hash the password here!
};
mockUsers.push(newUser);
console.log('User created:', newUser);
console.log('Current users:', mockUsers);
// Return the newly created user object (matching the 'User' type in schema)
return newUser;
},
},
};
export default resolvers; // Or module.exports = resolvers;

Key points:

  • Import GraphQLError from graphql.
  • Perform checks for username length, email format, and password length.
  • Collect all validation errors in an errors object.
  • If errors is not empty, throw a GraphQLError.
  • Use the extensions property of GraphQLError to provide structured error details (like code: 'BAD_USER_INPUT' and the specific validationErrors). This helps the client application display targeted feedback.
  • Only proceed with creating the user (mocked here) if validation passes.
  • Return the newly created user object, fulfilling the schema’s return type.

Integrate the schema and resolvers into your Apollo Server setup and run the server (npm start).

Open Apollo Sandbox (http://localhost:9000/graphql) and try the following mutation with invalid data:

mutation SignUpAttempt($inputData: SignUpInput!) {
signUp(input: $inputData) {
id
username
email
}
}

In the ‘Variables’ panel below the operation, enter invalid input:

{
"inputData": {
"username": "Al",
"email": "invalid-email",
"password": "short"
}
}

Execute the mutation. You should receive an error response containing the validation details:

{
"errors": [
{
"message": "Input validation failed",
"locations": [ ... ], // Indicates where the error occurred
"path": [
"signUp"
],
"extensions": {
"code": "BAD_USER_INPUT",
"validationErrors": {
"username": "Username must be at least 3 characters long.",
"email": "Please provide a valid email address.",
"password": "Password must be at least 8 characters long."
}
}
}
],
"data": null // Data is null because the mutation failed
}

Now try with valid data:

{
"inputData": {
"username": "ValidUser",
"email": "valid@example.com",
"password": "longenoughpassword"
}
}

This time, the mutation should succeed and return the created user data:

{
"data": {
"signUp": {
"id": "1",
"username": "ValidUser",
"email": "valid@example.com"
}
}
}
  • Use Input Types: Group arguments for mutations using input types.
  • Throw GraphQLError: Use standard GraphQL errors for validation failures.
  • Provide Details in Extensions: Use the extensions field to give clients structured information about what failed.
  • Validate Early: Perform validation before side effects (like database writes).
  • Consider Validation Libraries: For complex validation, consider using libraries like Yup, Joi, or class-validator within your resolvers.
  • Separate Concerns: In larger applications, you might extract validation logic into separate functions or modules for better organization and testability.