Authorisation belongs on resolvers
REST gives you a route per operation, so middleware can guard it. GraphQL gives you one endpoint and lets the client compose. Authenticating at the endpoint tells you someone is logged in, and nothing about whether they may read this particular field on this particular object.
Any authenticated caller reads any user.
Field-level too. A User type with an email field needs a check on that field, because a nested query can reach it through a path you didn't anticipate, a post's author, a comment's user.
Doing this per resolver by hand doesn't hold across a growing schema. Directives or a policy layer applied at schema level is the durable version.
Depth and complexity limits
Legal, and expensive. Without limits a single request can generate thousands of database queries.
Set a maximum depth, and a complexity budget that weights list fields by expected size. graphql-depth-limit and graphql-query-complexity both do this. Consider persisted queries in production, where the client sends an identifier for a query you've approved rather than arbitrary text.
Batching and aliases
Rate limiting by request count doesn't work here. One request can contain many operations:
That's a brute force in a single HTTP request. Limit aliases, disable batching on sensitive mutations, and rate limit on operations rather than requests.
Introspection and errors
Introspection publishes your entire schema. Useful in development, a map of your API in production. Disable it, along with GraphiQL and Playground.
Error messages leak too. Stack traces and database errors returned in the errors array are a common source of internal detail. Mask them in production and log the real thing server-side.
N+1 as a denial of service
The classic performance problem is also an availability one, a nested query can multiply into thousands of database round trips. DataLoader batching helps performance and reduces the ceiling of what a hostile query can cost.
Where to look
Standard scanners handle GraphQL poorly, because the vulnerable pattern is a missing check in a resolver rather than a recognisable construct. Dedicated tools like graphql-cop cover the configuration issues. Introspection, batching, depth limits, and the authorisation gaps still need review.