These 28 full-stack developer interview questions test whether an engineer can own a feature from the UI through the API to the database and into production. They cover API design, authentication and authorization, data modeling in Postgres, caching, background work, performance across the stack and shipping safely. Examples use TypeScript and Postgres because they are common, but every question works for other stacks. Ryz vets full-stack developers this way: recruiters source engineers who have shipped complete features, structured NTRVSTA AI interviews explore scenarios like these, and recruiters review every candidate. AI scores are advisory; people make the decisions.
Full-stack candidates are rarely equally strong on both sides, and that is fine. Use the questions to find where their depth is and whether their weaker side is still production-safe. A senior full-stack developer should handle the auth and security section without hesitation, because those mistakes cross every layer.
Favor questions that follow one feature across layers. "Where would you validate this, and why there?" reveals more than separate front-end and back-end quizzes.
Client-side validation, a request with credentials, server-side authentication, authorization and validation, a database transaction, a response with the saved resource, and a UI update from that response (or cache invalidation and refetch). Errors at each step need a path back to the user: field errors, conflicts, network failure.
What a strong answer shows: they name failure handling at each step, including double submits and a request that times out after the write succeeded.
The server is the authority; client validation is for fast feedback. Share one schema between both sides in a TypeScript stack, and keep database constraints (NOT NULL, unique, foreign keys, checks) as the last line of defense.
import { z } from "zod";
export const CreateProject = z.object({
name: z.string().trim().min(1).max(80),
visibility: z.enum(["private", "team", "public"]),
dueDate: z.coerce.date().optional(),
});
export type CreateProject = z.infer<typeof CreateProject>;
// in the route handler
const parsed = CreateProject.safeParse(await req.json());
if (!parsed.success) {
return Response.json({ errors: parsed.error.issues }, { status: 422 });
}
What a strong answer shows: they still rely on a unique constraint for uniqueness, because a pre-check query races with concurrent inserts.
For a first-party web app, a server-side session referenced by an HttpOnly, Secure, SameSite cookie is simple and revocable. JWTs suit stateless verification across services, but are hard to revoke and should not sit in localStorage, where any XSS can read them.
res.cookie("sid", session.id, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
What a strong answer shows: they pick based on revocation, XSS exposure and architecture, not on what is fashionable.
Nouns and HTTP methods: GET /projects, POST /projects, GET/PATCH/DELETE /projects/:id. Correct status codes (201 with the created resource, 404, 409 on conflicts, 422 on validation) and one consistent error body with a machine-readable code and field details, such as RFC 9457 problem details.
What a strong answer shows: they design for the client developer who must handle every error.
Run EXPLAIN (ANALYZE, BUFFERS) on the query. A sequential scan plus sort usually means a missing composite index that matches the filter and sort order.
SELECT id, total_cents, created_at
FROM orders
WHERE customer_id = $1 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
CREATE INDEX CONCURRENTLY orders_customer_status_created_idx
ON orders (customer_id, status, created_at DESC);
What a strong answer shows: equality columns before range or sort columns, and CONCURRENTLY to avoid blocking writes in production.
Static generation for content that changes rarely, server rendering for personalized pages that need fast first paint or SEO, and client rendering for highly interactive, authenticated app screens. Modern frameworks mix these per route, and server components move data fetching to the server by default.
What a strong answer shows: they reason about caching, SEO and time to interactive per route.
CORS lets a server tell browsers which other origins may read its responses. It doesn't stop requests from being sent, doesn't protect against CSRF by itself, and does nothing for non-browser clients. Allow specific origins, and never reflect any origin while also allowing credentials.
What a strong answer shows: they treat CORS as a browser read policy, not as authentication.
Offset is simple but gets slower on deep pages and skips or repeats rows when new items arrive. Cursor (keyset) pagination uses the last row's sort key and stays fast and stable.
SELECT id, title, created_at
FROM posts
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 25;
What a strong answer shows: they add a unique tiebreaker to the sort and encode the cursor opaquely for clients.
That is an N+1: one query for projects and one per owner. Fetch owners with a join or a single WHERE id = ANY($1) query, use the ORM's eager loading, or batch with a DataLoader in GraphQL resolvers. Add query counting in tests so it doesn't come back.
What a strong answer shows: they detect it with logs or tracing and prevent regression, not only fix the instance.
Require an idempotency key per logical operation. Claim the key atomically before doing work, and return the stored response for repeats.
const claimed = await db.query(
`INSERT INTO idempotency_keys (user_id, key, request_hash)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING key`,
[userId, key, requestHash]
);
if (claimed.rowCount === 0) {
const { rows } = await db.query(
"SELECT response_status, response_body FROM idempotency_keys WHERE user_id = $1 AND key = $2",
[userId, key]
);
if (rows[0].response_status === null) {
return res.status(409).json({ error: "request in progress" });
}
return res.status(rows[0].response_status).json(rows[0].response_body);
}
// perform the charge, then store response_status and response_body
What a strong answer shows: they handle the concurrent-retry race and reject a reused key with a different request body.
Have the server issue a short-lived presigned URL so the browser uploads directly to object storage, then confirm the upload through an API call or storage event. Validate type and size, scan if needed, and process thumbnails or parsing in a background job.
What a strong answer shows: they keep large bodies off the application servers and never trust the client's content type.
Update the cached data immediately, send the request, and roll back to the snapshot on error with a clear message. Data-fetching libraries such as TanStack Query provide hooks for this. Use it for low-risk, likely-to-succeed actions like toggles and reorders.
What a strong answer shows: they avoid optimistic UI for actions where a false success would mislead, such as payments.
Not in the request. Enqueue a job, and make sure the job is created only if the user row commits: write an outbox row in the same transaction and have a worker publish it, or use a Postgres-backed queue. Jobs must be idempotent and retried with backoff.
What a strong answer shows: they spot the dual-write problem between the database and the queue.
From the outside in: HTTP cache headers and a CDN for public assets and pages, an application cache such as Redis for expensive reads, and client-side caching in the data layer. Prefer short TTLs plus explicit invalidation on writes, and key caches by everything that changes the result, including tenant and permissions.
What a strong answer shows: they worry about serving one user's cached data to another.
Use the authorization code flow with PKCE through a maintained library. Send a state value to prevent CSRF, exchange the code server-side, validate the ID token (issuer, audience, expiry, signature), then link to a local user by the provider's stable subject ID, not by email alone. Create your own session afterward.
What a strong answer shows: they explain the account-takeover risk of linking accounts by unverified email.
SameSite=Lax or Strict blocks most cross-site requests, but add defense in depth for state-changing requests: a CSRF token, or verify the Origin header. Never change state on GET.
What a strong answer shows: they know SameSite's limits, such as same-site subdomains you don't fully control.
Scope every query by tenant in a shared data-access layer, test it with cross-tenant cases, and add a second layer in the database with Postgres row-level security.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (org_id = current_setting('app.current_org')::uuid);
-- per request, inside the transaction:
SELECT set_config('app.current_org', $1, true);
What a strong answer shows: they know table owners bypass RLS unless it is forced, so the app connects as a separate role.
Hash with a slow, salted algorithm such as Argon2id or bcrypt. Reset tokens are random, single-use, short-lived and stored hashed; the reset endpoint responds the same whether or not the email exists, and changing the password invalidates other sessions.
What a strong answer shows: they consider user enumeration and session invalidation, not just hashing.
Store a restricted format (Markdown or structured JSON) and render it through an allowlist sanitizer, or sanitize HTML on output with a maintained library. Rely on the framework's default escaping, avoid raw HTML injection, and add a Content Security Policy as a backstop.
What a strong answer shows: they sanitize at render time with an allowlist rather than trying to blocklist dangerous tags.
Limit login by account and by IP with a sliding window in a shared store like Redis, add backoff or a challenge after repeated failures, and alert on spikes. For the API, limit per key with clear 429 responses and Retry-After headers.
What a strong answer shows: they protect against credential stuffing without locking out real users permanently.
Clarify the rules (who can invite, roles, expiry, existing users versus new), model invitations as their own table with a hashed token and status, design endpoints, emails and UI states including errors, and cover abuse limits. Ship behind a flag, track invites sent and accepted, and write down the decisions.
What a strong answer shows: they surface product questions early and plan for edge cases like an invite to an email that already belongs to another workspace.
A shared schema with a tenant column is cheapest and simplest to operate. Schema per tenant or database per tenant gives stronger isolation and per-customer operations, such as dedicated regions, at higher operating cost. Many products start shared and move large customers to dedicated databases when contracts require it.
What a strong answer shows: they connect the choice to compliance, noisy neighbors and migration effort.
Polling is simplest and fine for slow-changing data. Server-sent events stream server-to-client over plain HTTP and reconnect automatically. WebSockets suit two-way, low-latency use like collaboration or chat. All long-lived connections need a pub/sub layer so any server instance can deliver a message.
What a strong answer shows: they choose the simplest option that meets the latency need and consider load balancer timeouts.
Start with a modular monolith: clear module boundaries, no cross-module table access and internal interfaces. Extract a service only when independent scaling, deployment or team ownership clearly justifies the network and operational cost.
What a strong answer shows: they evaluate team structure and operational maturity, not architecture fashion.
Measure real users' Core Web Vitals (LCP, INP, CLS) and look at a trace that spans browser, API and database. Split the time: network and payload size, server response, database, rendering and JavaScript execution. Fix the biggest piece first, then set a budget so it stays fixed.
What a strong answer shows: they use field data and traces instead of guessing from a fast laptop.
Ship it dark behind a flag evaluated on the server, enable it per tenant or percentage, watch error rates and product metrics for that cohort, and keep a kill switch. Remove the flag once it is fully rolled out.
What a strong answer shows: they plan flag cleanup and make sure both code paths are tested.
Options: generate a typed client from an OpenAPI spec, share types in a monorepo with an RPC layer such as tRPC, or use GraphQL code generation. Shared types catch drift at build time, but public APIs still need versioning and runtime validation.
What a strong answer shows: they distinguish internal clients they deploy together from external consumers they don't control.
The team's existing skills, hiring market, hosting and compliance needs, and how much the framework decides for you. Boring, well-supported tools usually win: one language across the stack, Postgres, a mainstream framework and a managed host.
What a strong answer shows: they optimize for shipping and maintainability, not novelty.
localStorage without considering XSS.Give a three to four hour take-home: build a small "shared shopping list" app with sign-in, lists owned by a household, and invites for other members. Provide a starter repo with the framework and Postgres in Docker. Require one list view with live or near-live updates, server-side authorization, a migration file and a few tests. Follow up with a 45-minute review where the candidate walks through the code and extends it.
Ryz introduces senior full-stack developers who take features from schema to UI to production. They are the top 1% of the candidates we interview, they join your team, standups and repos, and they work within ±1h of US time zones. Read about our vetting process or adapt our full-stack developer job description.
Splitting hides the skill you are hiring for: connecting the layers. Use one feature-shaped exercise that crosses the stack, then add a short focused round on whichever side the role leans toward.
Let them use their own stack for the exercise and focus questions on concepts: data modeling, auth, caching, error handling. Strong engineers transfer those quickly; framework syntax is the easy part.
A short take-home shows how someone structures a real app, which live coding rarely can. Keep it under four hours, pay for longer ones, and always follow with a live review so you can confirm the candidate understands and can extend their own work.
Questions we didn't answer? Email info@ryzlabs.com.