All jobs / Full-stack developer
End-to-End Architecture interview questions
End-to-End Architecture questions a Full-stack developer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
A list renders blank. The API returns `displayName`; the UI reads `name`.
What is the real fix?
The strongest answer: Agree the JSON contract — name, type and nullability — then change both sides to match it
Why it works: Exactly. A blank screen is a broken contract, not a CSS problem. Name, type and nullability are the join.
- The JSON is the join — agree it, then implement it twice.
- Write down name, type and whether it can be null.
- Change API and UI to that document, not to each other ad hoc.
- One unofficial alias is already a second contract.
Vite serves the UI on `:5173`. The API is on `:5088` of the same machine.
Do you need CORS?
The strongest answer: No — same site, same scheme, same host and port is one origin. CORS starts when any of those differ
Why it works: Exactly. Origin is scheme + host + port. `5173` and `5088` are different worlds to the browser.
- Origin = scheme + host + port.
- Same origin: no CORS conversation.
- Different port or host: the API must allow the page's origin.
- localhost does not get a free pass.
The UI on `localhost:5173` calls `api.example.test` and the browser blocks it as CORS.
Who owns the fix?
The strongest answer: The API must allow this origin. I say which header is missing and we change the server, not the user's browser
Why it works: Exactly. CORS is the browser enforcing the server's allow-list. The fix is the right `Access-Control-Allow-Origin` on the API.
- CORS is a server allow-list the browser enforces.
- Name the missing header and the origin that must appear.
- Fix the API (or the local proxy you own). Do not ask users to turn the check off.
- Never send `*` if the request carries cookies.
A form collects a meeting time. The API and a mobile client must store the same instant.
How do you send the date?
The strongest answer: UTC ISO-8601 on the wire (`2026-09-11T09:00:00Z`) and format for the user's locale only in the UI
Why it works: Exactly. One unambiguous format on the contract; locale is a view. ISO-8601 with an offset or Z is that format.
- Wire format is UTC ISO-8601.
- The UI formats for the reader.
- Do not send locale-shaped strings to the API.
- Pick one and put it in the OpenAPI, not in folklore.
The API sometimes omits `middleName`, sometimes sends `null`.
What does the contract say?
The strongest answer: Whether the field can be missing, null or `""` — those three are different, and both sides must treat them the same
Why it works: Exactly. Absent, null and empty string are three values. A contract that blurs them is a heisenbug.
- Say missing vs null vs empty.
- Do not let each side invent a default.
- Types in the repo are not the wire.
- Put the rule in OpenAPI or a shared schema.
The UI must send a PDF CV to the API.
How does the file get there?
The strongest answer: `multipart/form-data` (or a dedicated upload URL) — not a JSON string of the bytes
Why it works: Exactly. Files are a different body type. Multipart or a signed upload URL is the contract; JSON-of-bytes is a memory tax.
- Files travel as multipart or via a signed storage URL.
- Do not JSON-encode megabytes.
- The API never reads a path on the user's machine.
- Show progress; the request is no longer a tiny JSON POST.
The ticket is 'users can invite a teammate by email'. You own both sides.
How do you split the work?
The strongest answer: Agree the JSON and the screens, ship a stub API the UI can talk to, then fill the real logic — one vertical, not 'frontend waits a week'
Why it works: Exactly. The contract is the first deliverable. A stub lets both sides move without a wall.
- The contract is the first commit.
- Stub the API; render the screens.
- Fill logic behind the same shapes.
- Do not wait for a perfect backend to start the UI.
You add a `nickname` box. Today's API does not know that field.
What do you ship first?
The strongest answer: The API that accepts the new optional field, then the UI that sends it — old UI keeps working in between
Why it works: Exactly. Additive API first is how you survive cached JavaScript and a half-rolled deploy.
- Expand the API before the UI depends on it.
- New field starts optional.
- Ship UI second; old clients ignore the field.
- Never require a field the old bundle does not send.
Product wants in-app notifications: when someone comments on your document, you should see it.
What is the first version you build?
The strongest answer: A notification row per user per event, an endpoint the client reads plus a mark-as-read call, and the UI fetching it on load; push and email come later on top of the same record
Why it works: Exactly — the stored record is the product, and delivery is a detail you can add per channel without changing what a notification is.
- Store the notification, then deliver it.
- One row per user per event.
- Read and mark-as-read endpoints.
- Real-time and email are channels.
The bell in the header shows a count. A second tab shows a different number, and the phone shows a third.
Where should that number come from?
The strongest answer: From the server, counted from that user’s notification records, with mark-as-read going to the server too — every device asks the same source, so the badge agrees everywhere
Why it works: Right — one source of truth, read and written through the API, and the UI simply renders what the server says.
- The server owns the count.
- Count it from stored notifications.
- Mark-as-read goes to the server.
- Reconcile after a reconnect.
The SPA on `app.example.com` sends cookies to `api.example.com`.
Which CORS header set is correct?
The strongest answer: Echo the exact origin you trust, `Allow-Credentials: true`, and a tight method/header list — never `*` with credentials
Why it works: Exactly. The spec forbids `*` plus credentials. Name the origin. Reflecting every origin is `*` with extra steps.
- Credentials demand an explicit origin, not `*`.
- List methods and headers you actually use.
- Do not reflect arbitrary Origins.
- `no-cors` is not an API strategy.
A dashboard should show when an export job finishes. The job takes one to three minutes.
Which do you pick?
The strongest answer: Start with short polling or SSE if a few seconds of lag is fine; open a WebSocket when you truly need server-push at volume
Why it works: Exactly. A socket is a second protocol to auth, scale and reconnect. Earn it. SSE covers one-way push with less drama.
- Pick the cheapest channel that meets the lag.
- Polling or SSE first for 'did it finish?'.
- WebSocket when many updates must arrive without the client asking.
- Every extra protocol needs auth, retry and ops.
Web and iOS will read overlapping but different slices of the same customer graph.
Which do you pick?
The strongest answer: REST while the resources are clear and cacheable; GraphQL when many clients need different slices of a wide graph and over-fetch is the pain
Why it works: Exactly. This is a join decision, not a brand. Caching, file uploads and simple CRUD still love REST. Wide, client-specific reads love GraphQL.
- Choose for the clients you have, not the conference.
- REST for resources, HTTP cache and simple writes.
- GraphQL for many shapes over one graph.
- Do not duplicate every entity on both.
You are launching 'scheduled send'. The API route is new; the button is new.
How do you ship the flag?
The strongest answer: The same flag key on API and UI, default off, with the server rejecting the new payload while it is off — the UI being hidden is not enough
Why it works: Exactly. A hidden button with a live endpoint is a launch you cannot stop. One key, server-enforced.
- One flag, two readers, server is law.
- Default off; reject new payloads while off.
- Hide the UI as UX, not as the kill switch.
- Do not give each layer its own name for the same launch.
The web app must accept a large training video.
How should a 200MB video get to storage?
The strongest answer: The API mints a short-lived signed URL; the browser uploads to object storage; the API then accepts the object key after checks
Why it works: Exactly. Your API is a bad CDN. Sign, upload, confirm — then scan and process. The UI shows progress against storage, not against a 30s gateway.
- Large files bypass the app server.
- Short-lived signed URL, then a confirm callback.
- Scan and process after the object exists.
- Never ship a public writeable bucket.
The page needs a user, their last three invoices and a permission set. Those live on three internal services.
When do you add a BFF?
The strongest answer: When this UI needs a shape the public API should not have — aggregation, cookie-to-token translation, or hiding fields a mobile client must not see
Why it works: Exactly. A BFF is a tailored door for one client. Earn it with a real mismatch, not with fashion.
- BFF = this client's needs, not a second product.
- Use it to aggregate, adapt auth, or hide public-API edges.
- Do not invent one by default.
- Do not empty the real API into an untyped pass-through.
The server needs to push status updates to the browser, and the client never sends anything back on that channel.
Server-sent events or a WebSocket?
The strongest answer: Server-sent events — it is ordinary HTTP one way, so existing auth and proxies work and the browser reconnects and resumes on its own; take a WebSocket when you genuinely need both directions at low latency
Why it works: Right — one-way push over plain HTTP costs far less to operate, and the built-in reconnect with a last-event id is a class of code you never write.
- One-way push: server-sent events.
- Plain HTTP, existing auth and proxies.
- Reconnect and resume are built in.
- WebSocket when both directions matter.
You deploy API and UI. Support still sees errors from a field you renamed.
What do you assume after a deploy?
The strongest answer: Some tabs will run yesterday's JS against today's API for hours — so the API stays compatible and hashed assets get new names
Why it works: Exactly. Zero-downtime is a compatibility window, not a moment. Hash the bundle; keep the old routes answering the old JSON.
- Old JS will call the new API.
- Hashed filenames; do not cache `index.html` forever.
- Keep old fields until the window closes.
- Reload is not an error-handling strategy.
You need to change `amount` from a string to a number.
How do you version without stranding the UI?
The strongest answer: Prefer additive changes; if you must break, run v1 and v2 until the last old bundle is gone — do not flip a header on Tuesday and delete v1 on Tuesday
Why it works: Exactly. Version when the meaning changes. Overlap the window. A header that mutates JSON is a hidden second API.
- Additive first; versions for breaks.
- Overlap old and new until clients move.
- Do not mutate one URL by a client header.
- Hard-refresh is not a migration tool.
You want to rename `title` to `headline` on a resource the SPA already ships.
How do you roll the rename?
The strongest answer: Add the new field, deploy API, deploy UI that writes both or reads both, then drop the old field after the stale-bundle window — expand, migrate, contract
Why it works: Exactly. Expand/contract is how two deploys and a CDN coexist. The extra field is temporary on purpose.
- Expand (both fields), migrate (new UI), contract (drop old).
- API first on the expand.
- Time-box the overlap.
- A refresh banner is not a compatibility layer.
Growth wants a browser push prompt on the homepage so that more visitors opt in.
What do you say, and when should the prompt appear?
The strongest answer: Ask in context, right after an action that implies wanting updates, and explain why in our own UI first — the browser prompt is one shot per site and a refusal is effectively permanent
Why it works: Right — a pre-prompt you control can be asked again, the native one cannot, and browsers also quieten prompts that are not tied to a user action.
- Earn the ask before you spend it.
- Your own pre-prompt first.
- Trigger on a relevant user action.
- A block is effectively permanent.
Every click on a table of 40 rows fires OPTIONS then GET, and it shows on slow networks.
How do you stop the extra OPTIONS?
The strongest answer: Stay inside simple requests where you can, cache preflights with `Access-Control-Max-Age`, and stop sending custom headers on every GET
Why it works: Exactly. `Authorization` and `application/json` make a preflight. That is OK — just do not invent a third custom header on a list GET, and let the browser cache the OPTIONS.
- Know what triggers OPTIONS.
- Cache the preflight.
- Do not custom-header a public GET if a cookie or a query will do.
- Wildcards are not an optimisation.
You are tired of the form accepting a payload the API 400s.
Where does the schema live?
The strongest answer: One schema the form parses and the API executes — generated or imported — so a new required field fails both builds
Why it works: Exactly. Two implementations of one rule will diverge. The join is a single artefact in the build.
- One artefact, two runtimes.
- Generate or share; do not retell.
- CI fails if either side is behind.
- HTML attributes are a subset, not the schema.
The marketing layout SSR-renders a 'Your workspace' header for logged-in people.
How does the server render a logged-in page?
The strongest answer: The SSR request forwards the session cookie to the API (or calls in-process); it does not read `localStorage` — that does not exist on the server
Why it works: Exactly. SSR auth is request-scoped cookies or an internal call. localStorage is a browser fantasy. Embedding tokens in HTML is a cache and leak story.
- SSR sees cookies and headers, not localStorage.
- Forward the cookie or use an internal service call.
- Same user on the HTML and the hydrate.
- Tokens do not belong in markup.
Creators upload long videos from phones on bad networks.
How do you take a 2GB file on a train Wi-Fi?
The strongest answer: A resumable protocol (Tus or chunked signed parts) with a progress UI — not a single POST that dies at 90%
Why it works: Exactly. Large plus flaky means resume. The UI is a progress-and-retry surface; the API is parts and a complete call.
- Resume, parts, checksum, then assemble.
- Show progress against bytes accepted.
- Do not stream 2GB through the app server in one request.
- Complete is a separate, idempotent call.
Access tokens last 15 minutes. You need a stay-logged-in story.
How do you refresh the session?
The strongest answer: A rotating refresh cookie, HttpOnly, reused only once; reuse of an old refresh kills the family — access token stays short
Why it works: Exactly. Rotation detects theft: the stolen refresh works once, then the real user arrives and you revoke. Short access is the blast-radius knob.
- Short access, rotating refresh, reuse = revoke.
- Refresh cookie is HttpOnly.
- Do not mint decade-long access tokens for convenience.
- Third-party cookie assumptions are expired.
Users upload attachments other users can open in the app.
When may the UI show 'ready to download'?
The strongest answer: After the object is stored and a malware scan (and your own type check) passed — not as soon as the bytes landed
Why it works: Exactly. Upload ≠ safe. The UI should poll a status the API only flips after scan and content-type checks. Serving a 'PDF' that is HTML is an XSS on your origin.
- Store, scan, then mark ready.
- The UI binds to status, not to upload 200.
- Do not trust filename or client Content-Type.
- Serving user files from your app origin needs a tight content disposition.
The app opens a WebSocket for live comments after login.
Where does auth happen for the socket?
The strongest answer: On the upgrade (cookie or a one-time ticket), then you re-check authz on sensitive events — a connected socket is not a lifetime pass
Why it works: Exactly. The upgrade is an HTTP request: authenticate it. Then treat events as you would POSTs. Tickets beat putting secrets in query strings (those log).
- Authenticate the upgrade.
- Prefer cookies or a short-lived ticket, not `?token=` in logs.
- Authorise again on privileged events.
- Origin is not a session.