GraphQL Schema Input Validation using GraphQL Directives
Today, we delve into the world of GraphQL APIs and explore a crucial aspect of their development: schema input validation. As GraphQL continues to gain traction for its flexibility and efficiency in data querying, ensuring the integrity of your API's schema becomes paramount.
We'll be using these packages from NPM to implement schema validation. The main package to lookout for is graphql-constraint-directive. This package will allow us to apply GraphQL directives to enforce schema validation.
"@apollo/server": Version 4.10.3
"@graphql-tools/schema": Version 10.0.3
"express": Version 4.19.2
"graphql": Version 16.8.1
"graphql-constraint-directive": Version 5.4.3
"graphql-tag": Version 2.12.6
For this project, this will be our directory structure:
Github Repo:
https://github.com/17bcs1837/graphql-constraint-directive
Setting Up the Project
To get started, let's examine the index.js
file where we configure our GraphQL server. We begin by importing necessary dependencies such as express
for building our web server and ApolloServer
for creating our GraphQL server instance. Additionally, we import utilities from graphql-constraint-directive
to enable schema validation using directives.
index.js
const express = require("express");
const { ApolloServer } = require("@apollo/server");
const { expressMiddleware } = require("@apollo/server/express4");
const {
createApollo4QueryValidationPlugin,
constraintDirectiveTypeDefs,
} = require("graphql-constraint-directive/apollo4");
const { makeExecutableSchema } = require("@graphql-tools/schema");
const userTypeDefs = require("./graphql/typedefs");
const userResolvers = require("./graphql/resolvers");
async function graphqlServer() {
const app = express();
const apolloServer = new ApolloServer({
typeDefs: makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, userTypeDefs],
}),
resolvers: userResolvers,
introspection: true,
plugins: [createApollo4QueryValidationPlugin()],
});
await apolloServer.start();
app.use("/api/graphql", express.json(), expressMiddleware(apolloServer));
app.listen(3000, () => {
console.log(`GraphQL server running at http://localhost:3000/api/graphql`);
});
}
graphqlServer();
Notice how we pass:
constraintDirectiveTypeDefs
createApollo4QueryValidationPlugin
from graphql-constraint-directive/apollo4
This adds all the constraint directives to our schema and necessary logic to resolve them.
Defining GraphQL Schema and Resolvers
In this section, we'll define the GraphQL schema for a simple user management system and implement corresponding resolvers to handle queries and mutations.
User Type Definitions (./graphql/typedefs.js)
const { gql } = require("graphql-tag");
const userTypeDefs = gql`
type User {
id: Int
username: String!
email: String!
age: Int
createdAt: String!
updatedAt: String!
}
type Query {
getUser(id: Int! @constraint(max: 5)): User
getUsers: [User]
}
type Mutation {
createUser(
username: String!
email: String! @constraint(format: "email")
age: Int
): User
updateUser(id: ID!, username: String, email: String, age: Int): User
deleteUser(id: ID!): User
}
`;
module.exports = userTypeDefs;
We have added a constraint on getUser query argument id and createUser mutation argument email.
User Resolvers (./graphql/resolvers.js)
// Dummy user data
let users = [
{
id: 1,
username: "user1",
email: "user1@example.com",
age: 25,
createdAt: "2024-04-16",
updatedAt: "2024-04-16",
},
{
id: 2,
username: "user2",
email: "user2@example.com",
age: 30,
createdAt: "2024-04-16",
updatedAt: "2024-04-16",
},
];
const userResolvers = {
Query: {
getUser: (_, { id }) => users.find((user) => user.id === id),
getUsers: () => users,
},
Mutation: {
createUser: (_, { username, email, age }) => {
const newUser = {
id: String(users.length + 1),
username,
email,
age,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
users.push(newUser);
return newUser;
},
updateUser: (_, { id, username, email, age }) => {
const index = users.findIndex((user) => user.id === id);
if (index !== -1) {
if (username) users[index].username = username;
if (email) users[index].email = email;
if (age) users[index].age = age;
users[index].updatedAt = new Date().toISOString();
return users[index];
}
return null;
},
deleteUser: (_, { id }) => {
const index = users.findIndex((user) => user.id === id);
if (index !== -1) {
const deletedUser = users.splice(index, 1);
return deletedUser[0];
}
return null;
},
},
};
module.exports = userResolvers;
Finally, We can run our project:
node index.js
Let's open GraphQL playground and see our constraints in action:
- getUser Query: We'll pass id as 20. It should throw an error, because id cannot be greater than 5 as per our constraints.
- createUser Mutation: We'll pass email as 'email.com'. It should throw an error, because it is not a valid email.
Point To Remember
- If we have more than one constraint and both of them fails. Then the validation errors are returned under extensions.validationErrors:
💡
You can also implement custom error handler, to have all the errors under extensions.validationErrors like this:
const apolloServer = new ApolloServer({
typeDefs: makeExecutableSchema({
typeDefs: [constraintDirectiveTypeDefs, userTypeDefs],
}),
resolvers: userResolvers,
introspection: true,
plugins: [createApollo4QueryValidationPlugin()],
formatError: (formattedError, _error) => {
const msg = formattedError.message;
if (msg.startsWith("Variable") || msg.startsWith("Argument")) {
return {
...formattedError,
extensions: {
validationErrors: [formattedError],
},
};
}
return formattedError;
},
});
You can get the list of all the constraint directives here:
Subscribe to my newsletter
Read articles from Aman Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by