Full-Stack Questions

Hasura & GraphQL Interview Questions

How Hasura works, permissions, remote schemas, actions, events, subscriptions and GraphQL core ideas.

Lesson 0929 min read#hasura#graphql

On this page

  1. Words used in this lesson
  2. What Hasura is, and what it is not
  3. How Hasura turns GraphQL into SQL
  4. Hasura v2 and v3 (DDN)
  5. Hasura or Apollo Server? (Federation too)
  6. Tracking tables and relationships
  7. Roles, session variables and permissions
  8. Authentication: admin secret, JWT, webhook
  9. Extending Hasura: remote schemas, actions, triggers
  10. The development workflow: CLI, metadata and migrations
  11. Subscriptions and scaling
  12. Performance and security checklist
  13. GraphQL core: what you must know without Hasura
  14. Interview questions
  15. Key takeaways
  16. What's next

In one line: Hasura is a server that sits in front of your database and gives you a ready-made GraphQL API with permissions, without you writing resolvers. GraphQL is the query language it speaks. Interviewers test both: how Hasura works and where it stops, and whether you understand plain GraphQL (resolvers, N+1, errors, security, pagination) well enough to build or judge any GraphQL server.

You will see two Hasura products in job posts, and they are very different inside. So this lesson marks every Hasura statement with its version: (v2), (v3) or (both). Statements about GraphQL itself are marked (spec).

How this was checked. The Hasura facts come from the documentation and source in Hasura's public repository (v2 docs, the v3 engine folder and its changelog). I could not run the Hasura server in my sandbox, so its YAML and JSON examples are copied from the docs and not executed. All the plain GraphQL code in this lesson was run with graphql 16.14 and dataloader 2.2. The SQL ran on PostgreSQL 16.

Words used in this lesson

  • GraphQL is a query language for APIs where the client asks for exactly the fields it needs, in one request, and the server returns JSON in the same shape.
  • A schema is the typed description of your API: which types, fields and arguments exist. The client can read it, which is called introspection.
  • A resolver is a function that fetches the value for one field of the schema.
  • A query reads data, a mutation changes data, and a subscription pushes new results to the client over a long-lived connection (usually a WebSocket).
  • A fragment is a reusable piece of a query: fragment F on User { id name }. A directive is an @name marker that changes how a field runs, such as @skip(if: $flag).
  • A role is a name for a kind of user (user, admin). A session variable is a value that comes with the request about who is calling, such as the user id.
  • Metadata (in Hasura) is the saved configuration: tracked tables, relationships, permissions, actions and triggers.
  • A webhook is a URL on your server that another system calls when something happens.
  • Row-level security means each user sees only the rows they own, enforced by a rule on the data, not by hand-written if statements.

What Hasura is, and what it is not

Hasura (v2) is a server, written in Haskell, that connects to your database, reads its tables, and publishes a GraphQL API for them: queries with filters, sorting and pagination, aggregates, inserts, updates, deletes, and subscriptions. It checks permissions on every request. You configure it in a web Console or in YAML files; you do not write resolvers for the tables.

Think of it as a very fast, configurable API layer on top of a database.

Hasura isHasura is not
A GraphQL/REST API generator for data you already storeAn ORM or a database
A permission engine (roles, row filters, column rules)A place to write business logic in code
A way to join your data with other APIs (remote schemas, actions)A replacement for a backend service
A source of change events (event triggers) and realtime data (subscriptions)A message queue with exactly-once delivery

Business logic that does not fit a table operation lives outside: in Actions (calls to your REST endpoint), remote schemas (another GraphQL server) and event triggers (webhooks). Often the right design is: Hasura for simple CRUD (create, read, update, delete) with permissions, and a normal Node service for rules like "charge the card, then create the order".

How Hasura turns GraphQL into SQL

(v2) Hasura does not run a resolver for each field. It is a compiler. The FAQ in the docs describes the path: GraphQL parser -> GraphQL AST -> SQL AST -> SQL. (An AST, abstract syntax tree, is the parsed structure of a query.) The result is one SQL statement for a query of any depth, with the user's permission filter already inside it.

I cannot run Hasura here, but the docs' explain page shows it builds json_agg and row_to_json expressions. Here is the same idea in plain SQL that I ran on PostgreSQL: users with their orders in one statement.

SQL
SELECT coalesce(json_agg(row_to_json(u_row)), '[]') AS users FROM (  SELECT u.name,         (SELECT coalesce(json_agg(row_to_json(o_row)), '[]')            FROM (SELECT o.id, o.total FROM orders o WHERE o.user_id = u.id ORDER BY o.id) o_row) AS orders  FROM users u WHERE u.id <= 2 ORDER BY u.id) u_row;-- [{"name":"Ann","orders":[{"id":1,...},{"id":2,...}]}, {"name":"Bob","orders":[...]}]

This is not Hasura's exact output. It shows why the N+1 problem does not appear for database relationships: the nesting happens inside the database, in one round trip. The cost moves to the database, so indexes on the join and filter columns matter a lot. The Console has an Analyze button that shows the generated SQL and its plan.

N+1 can still come back through remote relationships (joins to another GraphQL server or API). The docs link the problem and say that Hasura works to reduce it, but a remote call is not a SQL join. Keep remote joins small.

Hasura v2 and v3 (DDN)

This confuses many candidates. They are two different products with the same name.

Hasura v2Hasura v3 / DDN
What it isThe original engine (Haskell). Latest v2 tag in the repo: v2.51.0A new engine (Rust) that powers Hasura DDN (Data Delivery Network)
Talks to the databaseDirectlyNever directly. It sends a query plan to a data connector that talks to the source
ConfigurationMetadata in a database, edited in the Console or by the hasura CLI (YAML)Metadata written in the OpenDD format and built with the ddn CLI
SchemaOne GraphQL schema per roleOne big schema built at build time, with role annotations
Data sourcesPostgres, SQL Server, BigQuery and othersPostgres, MongoDB, ClickHouse, SQL Server and more, through connectors
Extra logicActions, event triggers, scheduled triggers, remote schemasConnectors and commands (you run your own logic in a connector)
SubscriptionsLive queries and streamingAdded later (changelog v2024.11.18), polling every 1000 ms by default

Which one is widely used? Hasura v2 has been around since 2018 and runs in most existing projects, so most job posts and interview questions mean v2. The repository README calls v2 "the current stable version" and presents v3 as the future, with the engine code in a v3 folder. When an interviewer says "Hasura", I ask: "Is that v2 or DDN?" It shows that I know both exist. In the rest of this lesson, unmarked Hasura details are (v2).

I did not find event triggers or scheduled triggers as built-in v3 engine features in the repository, so if you move to DDN, plan to build that logic in your own services. Check the DDN docs before promising a migration.

Hasura or Apollo Server? (Federation too)

Hasura (v2)Apollo Server (you write the resolvers)
Time to a working CRUD APIMinutesDays
Custom logicOutside (actions, triggers)Inside resolvers
PermissionsDeclarative rules in metadataCode you write and test
N+1Solved by the SQL compiler for DB relationsYour job: DataLoader
Data sourceMostly SQL databases (plus remote schemas)Anything
FitData-heavy apps, dashboards, realtimeComplex domain logic, many different backends

Apollo Federation is a way to split one GraphQL API across several subgraphs (small GraphQL services) combined by a gateway or router. (v2) Hasura can act as a subgraph: it supports the Apollo Federation v1 spec from v2.10.0, and you must turn it on (HASURA_GRAPHQL_ENABLE_APOLLO_FEDERATION). The docs say other subgraphs can use Hasura table types, but Hasura types cannot be extended by other subgraphs; for that direction use Hasura remote relationships. Many teams simply mix them: Apollo or Yoga for custom domain services, Hasura for the data-heavy part.

Tracking tables and relationships

(v2) Hasura does not expose a table until you track it, which means telling Hasura "put this table in the API". Tracking a table creates root fields such as users, users_by_pk and users_aggregate (the _by_pk field reads one row by primary key; _aggregate gives counts, sums and similar). Hasura can track every table in a schema in one click, but I only track what the API needs.

Relationships connect tracked tables in the schema. An object relationship returns one related row (a post's author). An array relationship returns a list (an author's posts). Hasura can suggest them from foreign keys, or you define them by hand when a foreign key does not exist (for example on a view). Remote relationships join to a remote schema or another source.

Roles, session variables and permissions

This is the heart of Hasura, and the most common interview topic.

(v2) Every request has a role. A built-in admin role can do everything. You create other roles simply by adding a permission for that name. For each role Hasura publishes a different GraphQL schema that shows only what the role may use.

Permissions are set per table, role and operation (select, insert, update, delete):

  • Select: a row filter (a boolean rule), the allowed columns, aggregation permission, and an optional row limit.
  • Insert: a check (the new row must pass it), and column presets (values Hasura fills, such as user_id from the session).
  • Update: which rows can be updated, which columns, and a check on the result.
  • Delete: a row filter.

A row filter uses session variables. This metadata (from the docs' shape) lets a user read only their own rows, at most 100 at a time:

YAML
select_permissions:  - role: user    permission:      columns: [id, title]      filter:        user_id:          _eq: X-Hasura-User-Id      limit: 100

Session variables are values that start with X-Hasura-. Hasura only sees variables with that prefix, and they are case-insensitive. The most important are X-Hasura-Role and X-Hasura-User-Id.

Inherited roles (v2) let you build a role from two or more roles, so an editor who is also a reviewer gets both sets of rules. They cannot form a cycle.

The permission filter is added inside the SQL, so it cannot be skipped by the client. But a wrong filter is a data leak. Review the Console's permissions summary page before launch, and give the anonymous role very little.

Authentication: admin secret, JWT, webhook

Hasura does not log users in. It trusts an authentication service that you run (or buy) to prove who the caller is. (v2) It supports two modes, both of which require an admin secret to be set first.

  • Admin secret. A long password in the X-Hasura-Admin-Secret header gives full admin power. Only servers should have it. Never ship it to a browser.
  • JWT mode. A JWT (JSON Web Token) is a signed text token that carries claims. Your login service signs the token. Hasura checks the signature using HASURA_GRAPHQL_JWT_SECRET (a JSON config with key type, key or jwk_url, and so on). It then reads the claims under the namespace https://hasura.io/jwt/claims:
JavaScript
{  "sub": "1234567890",  "https://hasura.io/jwt/claims": {    "x-hasura-default-role": "user",    "x-hasura-allowed-roles": ["user", "admin"],    "x-hasura-user-id": "123"  }}

x-hasura-default-role and x-hasura-allowed-roles are required. The client may send an X-Hasura-Role header to pick one role from the allowed list. If it sends none, the default role is used. Claim values are strings; Hasura converts types.

  • Webhook mode. Hasura calls your URL (HASURA_GRAPHQL_AUTH_HOOK) with the client's headers (a GET by default, or POST if you configure it). Your service answers with the session variables. I use it when tokens are opaque or sessions must be revocable at once. The cost is one extra HTTP call per request, so cache it or keep it fast.
  • Unauthenticated access. With an admin secret set, Hasura rejects requests with no credentials, unless you define HASURA_GRAPHQL_UNAUTHORIZED_ROLE. Then those calls run as that role.

Extending Hasura: remote schemas, actions, triggers

Feature (v2)What it doesUse it for
Remote schemaMerges another GraphQL server into the same endpoint (the docs call it automated schema stitching)A CMS, a payment API, your own GraphQL service
ActionAdds a query or mutation whose logic is an HTTP handler you writeCustom business logic, calling third-party APIs
Event triggerCalls a webhook when a row is inserted, updated or deleted (Postgres, SQL Server)Send an email, update a search index
Scheduled triggerCalls a webhook on a cron schedule or at one set timeNightly reports, reminders

Details that interviewers like:

  • Event triggers are at least once and may arrive out of order. The docs say so. Your webhook must be idempotent (safe to run twice). The payload has event.op (INSERT, UPDATE, DELETE or MANUAL), event.data.old and event.data.new, the session variables and a trace context. Retry settings default to num_retries: 0, interval_sec: 10 and timeout_sec: 60. So retries are off by default; turn them on.
  • Actions can be synchronous, or asynchronous (mutations only). An async action returns an action_id right away, and Hasura creates a query and a subscription so the client can read the result later.
  • Event triggers store their events in the database, so two Hasura instances with different metadata on the same database can cause undefined behaviour.

The development workflow: CLI, metadata and migrations

(v2) Two kinds of change must be versioned in git:

  • Migrations are SQL files that change the database (up.sql and down.sql).
  • Metadata is the Hasura configuration, exported as a folder of YAML files (databases/, actions.yaml, cron_triggers.yaml, remote_schemas.yaml, rest_endpoints.yaml, allow_list.yaml and more).

The hasura CLI works with both: hasura migrate create, hasura migrate apply, hasura migrate status, hasura migrate squash, hasura metadata export, hasura metadata apply, hasura metadata inconsistency list. hasura console opens a Console that writes your changes to local files as you click, which is the recommended dev setup. The docs say to turn the server's own Console off in production. hasura deploy is listed as a preview command.

For deployment there is a cli-migrations Docker image. When it starts, it applies the mounted migrations and metadata, then serves traffic. The docs say it is meant for automated deployments, not as a separate init container.

Metadata inconsistency. If you drop a column that a permission or relationship still uses, Hasura marks the metadata as inconsistent. List it with hasura metadata inconsistency list and fix or drop the broken item. Always change the database and the metadata together, in one release.

Subscriptions and scaling

(v2) A Hasura subscription is a live query: you get the latest result of the query, not each individual event. Hasura does this by polling, not by reading the database log. To keep it cheap it uses three techniques from the docs:

  1. Multiplexing. Subscriptions with the same role and the same selection set (different only in scalar values, like id) are combined into one SQL query with many parameter sets. They run in batches (HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_BATCH_SIZE, default 100). So 1000 similar subscribers need about 10 SQL queries per refresh, not 1000.
  2. Refetch interval. The combined query runs every 1 second by default.
  3. Diff check. Hasura compares old and new results and sends data only to subscribers whose result changed.

Things that break multiplexing: a different role, a different selection set, or even a different alias. Streaming subscriptions (from v2.7.0) send only new rows, using a cursor column.

To scale: add Hasura instances behind a load balancer (no sticky sessions needed), add database read replicas where your edition supports routing to them, add indexes, and select fewer fields (every joined field is more work per second). Watch the number of active subscriptions and the database connections.

Performance and security checklist

(v2) The production checklist in the docs is a good answer by itself:

  • Set an admin secret. Keep the Console off and dev mode off (both are off by default). Dev mode would show internal error details to clients.
  • Enable only the GraphQL API (HASURA_GRAPHQL_ENABLED_APIS=graphql) so metadata and other APIs are not reachable.
  • Set a row limit in select permissions, so nobody can fetch a whole table.
  • Restrict CORS (HASURA_GRAPHQL_CORS_DOMAIN). By default all origins are allowed.
  • Use an allow list (HASURA_GRAPHQL_ENABLE_ALLOWLIST=true): only saved operations run. The admin role is not checked.
  • Set pool sizes so Hasura does not use up the database's connections.

Some protections depend on the edition:

Feature (v2)Community Edition (free, self-hosted)Cloud (paid tiers) / Enterprise
Row limits, allow list, admin secret, JWT, permissionsYesYes
API limits: rate, depth, node count, time, batch sizeNoYes (rate limiting needs Redis on self-hosted Enterprise)
Response caching with the @cached directiveNoYes
Disable introspection per roleNo (the docs mark it Cloud and Enterprise)Yes

So for a free self-hosted setup, depth and rate limiting happen in a reverse proxy or gateway in front of Hasura. The cache key (when caching is on) is a hash of the query text, operation name, variables, and the role and session variables used by the permissions. Two users with different X-Hasura-User-Id values do not share cache entries when their permissions use that variable.

REST endpoints (v2). Hasura can turn a saved query or mutation into a RESTified endpoint, with an OpenAPI export. For pagination use limit and offset, or keyset pagination with _gt on an ordered unique column.

GraphQL core: what you must know without Hasura

Schema, resolvers and execution (spec)

A server has a schema, and a resolver for each field that is not a plain property. The server walks the query tree and calls resolvers. The spec says query fields may run in parallel, but mutation root fields run one after another, so mutation { a b } always runs a first.

The N+1 problem and DataLoader

If a books list resolves author per book and each call hits the database, 3 books make 3 author queries (plus 1 for the list). DataLoader collects all load(id) calls made in the same tick and sends one batch call. I ran both versions on 3 books:

JavaScript
const loader = new DataLoader(async (ids) => {  calls++;                                   // one call for the whole batch  return ids.map((id) => authors.find((a) => a.id == id));});// author resolver: return context.loader.load(book.authorId)

Result: naive = 3 author lookups, DataLoader = 1. Create the loader per request, never globally, or its cache will show one user's data to another (Backend, Lesson 08).

Fragments, variables and directives (spec)

Use variables (query($id: ID!) { ... }) instead of building query strings: they are typed and safe. Fragments remove repeated selections and match component data needs. @skip(if:) and @include(if:) are built in; @deprecated marks old fields. A missing required variable is a request error: Variable "$id" of required type "ID!" was not provided.

Input validation

GraphQL types check shape, not business rules. Validate the rest in resolvers or with Zod (Backend, Lesson 03). The @oneOf input rule (exactly one field set) is in the spec draft, and graphql 16.14 enforced it in my test.

Errors (spec)

There are two kinds:

  • A request error (bad syntax, an unknown field, wrong variable): the response has errors and no data at all. I got Cannot query field "nope" on type "Book" with line and column.
  • A field error (a resolver threw): the response has data with that field set to null, plus an errors entry with a path.

Non-null propagation: if a field typed String! fails, null is not allowed there, so the null moves up to the nearest nullable parent. In my test the list was [Book!]! and the field secret: String!. One error nulled the whole data. So make fields nullable when they can fail, such as fields that call another service.

HTTP status codes. Classic servers return 200 even for errors. The GraphQL over HTTP draft spec adds the media type application/graphql-response+json: request errors then use 4xx (for example 400 for a bad document, and 422 is recommended for a failed validation), and a 200 is for an executed request. It is a draft, so check what your client and server support.

Security

  • Depth and complexity limits. Deep queries like books { author { books { author ... } } } can make a server do huge work. I wrote a small validation rule and it rejected a depth-6 query with Depth 6 exceeds 4. Also limit the count of aliases and the batch size: I put 5 aliases of the same field in one request and all 5 ran, so "requests per minute" alone does not limit work. Count cost per operation.
  • Introspection can be turned off in production with NoSchemaIntrospectionCustomRule from graphql. It only hides the schema, so it is not real security.
  • Authorization in the data layer, not only in the schema.
  • Persisted queries (see below).

Persisted queries

A persisted query is a query saved on the server and called by an id (often a hash). The client sends { id } and the server runs only known queries. The benefits are smaller requests and a built-in allow list. In a test I stored { books } under its SHA-256 hash: the known id returned data with status 200, and an unknown id got 400. Persisted queries are not part of the GraphQL spec; they come from tools (Apollo, Relay, Hasura's allow list). Hasura's allow list is the same idea.

Pagination: Relay cursor vs offset

Offset (limit, offset)Cursor / Relay connection
SimpleYesMore fields
Deep pagesSlow (database skips rows)Fast (starts at the cursor)
Rows added meanwhileDuplicates or gapsStable
Jump to page 50EasyHard

The Relay Cursor Connections spec shape is edges { node cursor } plus pageInfo { hasNextPage endCursor ... }. The cursor is an opaque text, often base64. I built one and paged 7 rows as [1,2,3] then [4,5,6], with hasNextPage: true after the first page.

Codegen

Codegen generates TypeScript types from your schema and your queries. I ran GraphQL Code Generator (@graphql-codegen/cli with the typescript and typescript-operations plugins) on a small schema. For the query { books(first: 5) { id title rating author { name } } } it produced:

JavaScript
export type BooksQuery = { books: Array<{ id: string, title: string, rating: number | null, author: { name: string } }> };

Note rating: number | null (nullable in the schema) and id: string (the ID scalar). With Hasura you point codegen at the Hasura endpoint, with an admin secret in the header, or at one role's schema.

Interview questions

What is Hasura, and what is it not? (must know)

(v2) Hasura is a server that sits on top of a database and generates a GraphQL API for the tables I track, with permissions on every request. It also gives me subscriptions, event triggers and REST endpoints. It is not an ORM and not a place to write business logic. Logic lives outside, in actions, remote schemas, event triggers or a normal service. I use it for data-heavy CRUD and put the real domain rules in Node.

Hasura v2 or v3? Which one is used more? (must know)

They are different products. v2 is the original Haskell engine from 2018: it talks to the database directly, I manage it with the Console and the hasura CLI, and it has actions, event triggers and scheduled triggers. v3 / DDN is a Rust engine: it never talks to the database itself, it uses data connectors, metadata is written in OpenDD and built with the ddn CLI, and it builds one schema with role annotations. Most existing projects and most job posts mean v2, and the repo README calls v2 the current stable version. I always ask which one the team means.

How does Hasura avoid the N+1 problem? (must know)

(v2) It is a compiler, not a set of resolvers. A GraphQL query becomes one SQL statement with nested json_agg expressions, with the permission filter inside. So nested relationships take one round trip. The work moves into the database, so I add indexes on foreign keys and filter columns, and I use the Console's Analyze to read the plan. The N+1 risk comes back with remote relationships, because those make calls to another server.

Explain roles and session variables in Hasura. (must know)

(v2) Every request has a role (X-Hasura-Role, or the default role from the JWT). Permissions are defined per table, role and operation. Session variables are X-Hasura-* values, like X-Hasura-User-Id. In a row filter I write user_id: { _eq: X-Hasura-User-Id }, and Hasura puts that into the SQL WHERE. For inserts I use a column preset so the client cannot set another user's id. Each role even sees its own schema, so unauthorised fields are not visible.

How does JWT authentication work in Hasura? (must know)

(v2) My login service signs a JWT. Hasura verifies it with HASURA_GRAPHQL_JWT_SECRET (a key or a jwk_url). It reads claims under https://hasura.io/jwt/claims. Two are required: x-hasura-default-role and x-hasura-allowed-roles. The client can choose one allowed role with the X-Hasura-Role header. Everything else, like x-hasura-user-id, is optional and can be used in permissions. Claim values are strings. An admin secret must be set first, or JWT mode does not apply.

Action, event trigger, remote schema: which one for what? (must know)

(v2) An action is for a request-time operation that needs my logic and returns a result, such as "create a payment intent". An event trigger is for something that must happen after a row changes, such as "send a welcome email", and it runs in the background. A remote schema is for joining a whole other GraphQL API into this one. If the logic is complex, I keep it in a normal service and call it from an action.

Can you rely on event triggers for exactly-once processing? (trap)

No. (v2) The docs say delivery is at least once and events may arrive out of order. Retries default to zero, so I also set num_retries. My handler must be idempotent: I store the event id and skip it if I have seen it, or I design the change so repeating it is harmless. If order matters, I read the current row state from the database instead of trusting the payload order.

How do Hasura subscriptions work and how do they scale? (senior signal)

(v2) They are live queries by polling. Subscriptions with the same role and selection set are multiplexed into one SQL query that runs for a batch of subscribers (batch size default 100) every second by default. Hasura diff-checks results and sends only changes. To scale I add instances (no sticky sessions needed), add indexes, select fewer joined fields and use read replicas if the edition supports it. If one client uses an alias that others do not, it will not multiplex with them, so I keep subscription shapes consistent.

How do you manage Hasura changes across dev, staging and production? (must know)

(v2) Migrations (SQL) and metadata (YAML) both live in git. In dev I run hasura console so my Console clicks are written to local files. In CI/CD I apply with hasura migrate apply and hasura metadata apply, or I use the cli-migrations image that applies them at start. I check hasura migrate status and hasura metadata inconsistency list. The database and metadata must change together, or I get inconsistent metadata.

What would you check before putting Hasura in production? (senior signal)

(v2) Admin secret set and never in a browser; Console and dev mode off; only the GraphQL API enabled; a row limit on every select permission; restricted CORS; an allow list for public apps; HTTPS; sensible pool sizes; indexes for permission filters; and monitoring. I also know which protections need a paid tier (API limits, caching, per-role introspection control), so on the free edition I put depth and rate limits in a gateway.

A client sends a very deep, expensive query. How do you protect the API? (trap)

Depth limits, node or complexity limits, time limits, and a cap on batch size and aliases. (v2) Hasura has these as API limits, but only on Cloud paid tiers and Enterprise, not Community. On the free edition I use an allow list (so only known operations run) and a row limit, and I add depth checks in a proxy. For my own GraphQL server, I add a validation rule: mine rejected depth 6 when the limit was 4. And I remember that rate limiting by request count is weak because aliases put many calls in one request.

What does the response look like when a resolver throws? What is non-null propagation? (must know)

(spec) A field error gives partial data: the failed field is null and errors has an entry with a path. But if that field is declared non-null (String!), null is illegal, so it moves up to the nearest nullable parent. In my test, one failing secret: String! inside [Book!]! made the whole data null. A request error, like an unknown field, has no data at all. So I make fields nullable when they can fail, and I use the extensions field for error codes.

How do you fix the N+1 problem in a normal GraphQL server? (must know)

I batch with DataLoader. The author resolver calls loader.load(authorId). DataLoader waits for the current tick, collects all ids, and calls my batch function once with the list. I ran 3 books: 3 lookups without it, 1 with it. The loader is created per request. The batch function must return results in the same order as the ids. If the data is in SQL, I may also use a join in the parent resolver.

Relay cursor pagination or offset pagination? (senior signal)

For a small admin table, offset is fine and lets me jump to a page. For feeds and big tables I use cursors: the database starts from the cursor, so deep pages stay fast, and new rows do not cause duplicates. The Relay connection shape is edges { node cursor } and pageInfo { hasNextPage endCursor }. The cursor is opaque so I can change what is inside. In Hasura v2, I use limit and offset, or keyset pagination with _gt on an ordered unique column.

What are persisted queries, and are they part of GraphQL? (trap)

A persisted query is saved on the server and called by an id or hash, so the client sends only { id }. It reduces request size, and if the server only runs known ids it acts as an allow list. It is not in the GraphQL spec; it comes from tooling. I made a small test where a known id returned data and an unknown id returned 400. Hasura's allow list gives a similar protection.

Which HTTP status code does a GraphQL error return? (trap)

Many people say "always 200", and that is how many servers behave with application/json. The GraphQL over HTTP draft adds application/graphql-response+json. With it, request errors (a bad document or failed validation) use 4xx, and an executed request, even with field errors, uses 2xx. It is a draft, so I check what my server and client do and I never write client logic that relies only on the status code. I always read errors too.

How do you keep client types in sync with the GraphQL schema? (must know)

I use GraphQL Code Generator. It reads the schema and my queries and writes TypeScript types, so a schema change breaks the build instead of production. I ran it: a nullable rating: Float became number | null and ID! became string. I run codegen in CI and fail the build if the generated files change but are not committed. With Hasura, I point it at the endpoint with the role I want to type.

Key takeaways

  • Always say v2 or v3/DDN. They are different products. Most existing projects are v2.
  • Hasura is a compiler: GraphQL to one SQL statement. It avoids N+1 for database relations, so index your columns.
  • Permissions per role, table and operation use session variables. Use JWT claims or a webhook to set them.
  • Event triggers are at least once and retries are off by default. Make handlers idempotent.
  • Subscriptions are polling live queries, made cheap by multiplexing.
  • API limits, caching and per-role introspection control are not in Community Edition (v2).
  • In plain GraphQL: use DataLoader per request, understand request vs field errors and non-null propagation, limit depth and aliases, and prefer cursors for big lists.

What's next

Back to the data layer in Lesson 08: Prisma & SQL, or look at how API design choices carry over in Backend, Lesson 02.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Related series