Full-Stack Questions
Hasura & GraphQL Interview Questions
How Hasura works, permissions, remote schemas, actions, events, subscriptions and GraphQL core ideas.
On this page
- Words used in this lesson
- What Hasura is, and what it is not
- How Hasura turns GraphQL into SQL
- Hasura v2 and v3 (DDN)
- Hasura or Apollo Server? (Federation too)
- Tracking tables and relationships
- Roles, session variables and permissions
- Authentication: admin secret, JWT, webhook
- Extending Hasura: remote schemas, actions, triggers
- The development workflow: CLI, metadata and migrations
- Subscriptions and scaling
- Performance and security checklist
- GraphQL core: what you must know without Hasura
- Interview questions
- Key takeaways
- 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@namemarker 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
ifstatements.
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 is | Hasura is not |
|---|---|
| A GraphQL/REST API generator for data you already store | An 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.
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 v2 | Hasura v3 / DDN | |
|---|---|---|
| What it is | The original engine (Haskell). Latest v2 tag in the repo: v2.51.0 | A new engine (Rust) that powers Hasura DDN (Data Delivery Network) |
| Talks to the database | Directly | Never directly. It sends a query plan to a data connector that talks to the source |
| Configuration | Metadata 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 |
| Schema | One GraphQL schema per role | One big schema built at build time, with role annotations |
| Data sources | Postgres, SQL Server, BigQuery and others | Postgres, MongoDB, ClickHouse, SQL Server and more, through connectors |
| Extra logic | Actions, event triggers, scheduled triggers, remote schemas | Connectors and commands (you run your own logic in a connector) |
| Subscriptions | Live queries and streaming | Added 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 API | Minutes | Days |
| Custom logic | Outside (actions, triggers) | Inside resolvers |
| Permissions | Declarative rules in metadata | Code you write and test |
| N+1 | Solved by the SQL compiler for DB relations | Your job: DataLoader |
| Data source | Mostly SQL databases (plus remote schemas) | Anything |
| Fit | Data-heavy apps, dashboards, realtime | Complex 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_idfrom 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:
select_permissions: - role: user permission: columns: [id, title] filter: user_id: _eq: X-Hasura-User-Id limit: 100Session 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-Secretheader 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 orjwk_url, and so on). It then reads the claims under the namespacehttps://hasura.io/jwt/claims:
{ "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 (aGETby default, orPOSTif 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 does | Use it for |
|---|---|---|
| Remote schema | Merges another GraphQL server into the same endpoint (the docs call it automated schema stitching) | A CMS, a payment API, your own GraphQL service |
| Action | Adds a query or mutation whose logic is an HTTP handler you write | Custom business logic, calling third-party APIs |
| Event trigger | Calls a webhook when a row is inserted, updated or deleted (Postgres, SQL Server) | Send an email, update a search index |
| Scheduled trigger | Calls a webhook on a cron schedule or at one set time | Nightly 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,DELETEorMANUAL),event.data.oldandevent.data.new, the session variables and a trace context. Retry settings default tonum_retries: 0,interval_sec: 10andtimeout_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_idright 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.sqlanddown.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.yamland 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:
- 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. - Refetch interval. The combined query runs every 1 second by default.
- 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, permissions | Yes | Yes |
| API limits: rate, depth, node count, time, batch size | No | Yes (rate limiting needs Redis on self-hosted Enterprise) |
Response caching with the @cached directive | No | Yes |
| Disable introspection per role | No (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:
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
errorsand nodataat all. I gotCannot query field "nope" on type "Book"with line and column. - A field error (a resolver threw): the response has
datawith that field set tonull, plus anerrorsentry with apath.
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 withDepth 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
NoSchemaIntrospectionCustomRulefromgraphql. 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 | |
|---|---|---|
| Simple | Yes | More fields |
| Deep pages | Slow (database skips rows) | Fast (starts at the cursor) |
| Rows added meanwhile | Duplicates or gaps | Stable |
| Jump to page 50 | Easy | Hard |
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:
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
- REST vs GraphQL (vs Hasura): How to Choose · Backend in Depth
- GraphQL and gRPC: When Not to Use REST · API Design
Related series