Best Practices: Pagination, Errors and Security
Pagination
Never return an unbounded list. The community standard is cursor-based (connection) pagination, which returns edges plus page info and handles large, changing datasets well:
query {
posts(first: 10, after: "cursor123") {
edges { node { title } cursor }
pageInfo { hasNextPage endCursor }
}
}
Error handling
A GraphQL response can contain both data and an errors array – a query can partially succeed. Return typed, meaningful errors with an error code in the extensions, and do not leak stack traces to clients.
{
"data": { "user": null },
"errors": [{ "message": "User not found",
"extensions": { "code": "NOT_FOUND" } }]
}
Guard against expensive queries
Because clients compose their own queries, they can ask for something enormous – deeply nested or huge lists. Protect the server:
- Depth limiting – reject queries nested beyond a maximum.
- Query complexity analysis – assign a cost to fields and cap the total.
- Pagination limits – enforce a maximum
firstvalue. - Timeouts – abort long-running resolvers.
Authentication and authorization
Authenticate at the transport layer (e.g. a JWT in the HTTP header), put the user in the resolver context, and authorize inside resolvers – checking permissions per field where needed. The single endpoint does not change this; it is the same security you would apply to REST.
Key points
- Use cursor/connection pagination; never return unbounded lists.
- Return typed errors with codes; a response may hold both data and errors.
- Limit query depth and complexity to stop abusive requests.
- Authenticate at the transport, carry the user in context, authorize in resolvers.