I'm trying my hand at (Apollo) GraphQL on the server side and have been having a probably silly issue. I'm trying to sign up a user, but keep getting the error shown in the linked image below. What is the problem? Ignore the very simple auth flow, as I'm just testing out the GraphQl
Here are the relevant code snippets:
Schema
export default `
type User {
id: ID!
name: String!
email: String!
}
type Query {
allUsers: [User]
currentUser: User
}
type Mutation {
createAccount(name: String!, email: String!, password: String!): User
loginUser(email: String!, password: String!): User
updatePassword(email: String!, password: String!, newPassword: String!): User
deleteAccount(email: String!, password: String!): User
}
`
Resolvers
createAccount: async (
parent,
{ name, email, password },
{ User },
info
) => {
try {
// Check for invalid (undefined) credentials
if (!name || !email || !password) {
return 'Please provide valid credentials';
}
// Check if there is a user with the same email
const foundUser = await User.findOne({ email });
if (foundUser) {
return 'Email is already in use';
}
// If no user with email create a new user
const hashedPassword = await bcrypt.hash(password, 10);
await User.insert({ name, email, password: hashedPassword });
const savedUser = await User.findOne({ email });
return savedUser;
} catch (error) {
return error.message;
}
},
The biggest problem with your resolver is that in any number of scenarios, instead of returning a User
object, you return a string. Your schema specifies that createAccount
can return a User
or null
(if it was User!
, it would be non-nullable and then null
would not be a valid type either).
When you return a string in your resolver, because it's expecting an object, it coerces it into one and then starts looking for User
properties on that object (like name
and email
). Those properties don't exist, and because they are non-null properties on your User
object, returning null/undefined for them results in an error.
Your resolver should probably just throw whatever errors it needs to throw. Then they will be returned as part of the errors
array in your response. For example:
// Check if there is a user with the same email
const foundUser = await User.findOne({ email })
if (foundUser) throw new Error('Email is already in use')
// If no user with email create a new user
const hashedPassword = await bcrypt.hash(password, 10);
await User.insert({ name, email, password: hashedPassword });
const savedUser = await User.findOne({ email });
return savedUser;
Now, if you have a duplicate email, the response will look something like this:
{
"data": {
"createAccount": null
},
"errors": [
{
"message": "Email is already in use",
"locations": [
{
"line": 4,
"column": 3
}
],
"path": [
"createAccount"
]
}
]
}
If you want to manipulate how your errors are shown on the client, you should utilize the formatError
or formatResponse
configuration options for your Apollo server middleware. It's also good practice to use custom errors, allowing you to add custom properties like code
to more easily identify the error type on the client side.
Lastly, it's unnecessary to check if name, email or password are defined inside your resolver -- your schema already has these inputs marked as non-null, which means GraphQL will automatically return an error if any of them are missing.