Build the mental model
GraphQL schemas have no built-in auth mechanism: authentication (who is this) happens once during context creation by decoding a token, while authorization (can this user do this) must be an explicit check written into each resolver that needs it. Throw unauthenticated or unauthorized failures as `GraphQLError` with `extensions.code: 'UNAUTHENTICATED'` or `'FORBIDDEN'` so the client can distinguish them.
Connect it to a real scenario
Inside the `deleteTutorial` mutation resolver, throw `UNAUTHENTICATED` immediately if `context.userId` is missing, and throw `FORBIDDEN` if the user lacks the `editor` role — both checks must run before the database delete is called.
Try the working example
const resolvers = {
Mutation: {
deleteTutorial: (
_parent: unknown,
args: { id: string },
context: Context,
) => {
if (!context.userId) {
throw new GraphQLError('You must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (!context.roles.includes('editor')) {
throw new GraphQLError('Editors only', {
extensions: { code: 'FORBIDDEN' },
});
}
return tutorialRepository.delete(args.id);
},
},
};You can correctly write both authentication and authorization checks inside a resolver.5-minute try-it
Write the `publishTutorial` mutation with an authorization rule allowing an `author` (own tutorials only) or an `editor` (any tutorial).
One important caution
Do not enforce authorization only in the frontend UI (hiding a button) without a server-side check in the resolver — anyone can send a raw GraphQL request, so server-side enforcement is mandatory.