#Device API design questions
AddedThis page is for the last few days before an interview, when you no longer need to learn a topic but need to say it well, quickly. Every question here is one an interviewer for this role might ask (web UI and REST/Telnet APIs for a video-conferencing device platform, a team in India, an OS migration underneath), or one you should be able to answer for yourself in the middle of a design discussion. The first paragraph of each answer is the version you'd say: 30 to 90 seconds, the decision first and the reason straight after. The walkthrough under it is the reasoning you need when the follow-up is "why?", and the link at the end goes to the topic that teaches it properly. Every answer agrees with the chapter it links to; product facts about Poly are only the public ones those chapters cite.
Think of it as a deck of flashcards with the working shown. Read the question, answer it out loud in your own words, then open the answer and compare: did you name the decision, the mechanism or number behind it, and the trade-off? If one of the three is missing, that's the card to go back to. The few questions where arithmetic or behaviour matters have small runnable models; their output was produced on Node 26 and is re-checked every time the site is validated.
This first group covers the API itself and the UI that is its first client: modelling a device as resources, methods and status codes, PATCH, long-running operations, ETags, errors, bulk configuration, constrained devices, OpenAPI from spec to contract test, and the admin web UI's forms, apply flows, updates and offline states.
Show answer
Three drawers, plus identity and collections: what the admin chose, what the device reports, and what you asked it to do. "/device for identity, read-only; /settings for configuration, split into sections like /settings/audio and /settings/network with GET and PATCH; /status for reported state, read-only, because it changes every second and nobody sets it; collections for things with their own lifecycle, /peripherals created by the device and /calls created by clients dialling; and /operations for reboots and updates, which come back as 202 with an operation to poll. Then the cross-cutting rules: one error format, the major version in the path, units in field names, stable peripheral ids across reboots."
- Settings, status and operations have different writers, change rates, permissions and persistence: an admin writes settings rarely and they survive reboots and exports; the device writes status constantly; operations take time and may fail. AWS IoT's Device Shadow makes the same split with
desiredandreported. - Singletons (
/settings,/status,/device) have no id and supportGETandPATCH, neverPOSTorDELETE: you can't create a second set of settings. - Placing a call is creating a resource:
POST /callsreturns201and aLocation;DELETE /calls/{id}hangs up, and a secondDELETEreturning404means "already done". - Naming rules save partners pain for years:
lowerCamelCase, units in the name (sleepAfterMinutes,temperatureC), enums as lowercase strings, RFC 3339 UTC timestamps. - The two designs to avoid are RPC paths (
POST /setVolume) and one giant/configmixing settings, status and identity, whose ETag changes every time the temperature does.
Learn more: Modelling a device as resources.
/config simpler?Show answer
Because they have different writers and different lifetimes, and mixing them breaks concurrency, auditing and export. "If live state like the temperature or the mute button sits in the settings document, its ETag changes every few seconds, so every conditional write fails; pressing mute shows up in the audit log as a configuration change; and a configuration import wipes runtime state. Live mute is runtime state you can write, so it gets its own resource, PUT /audio/mute with true, while 'mute when a call starts' is a setting, muteOnJoin under /settings/audio."
- Ask the three-drawer question of each field: did an admin choose it, does the device report it, or is it something you do?
- An ETag over a document that includes changing status is useless for
If-Match: the tag has moved on before the admin's write arrives. - Settings are saved to flash, exported and audited; runtime state must not be. One resource can't have both behaviours.
- Writable runtime state is fine as its own resource, written as absolute state (
PUT … true) so a retried request can't toggle it back. - For comparison, Poly's public VideoOS REST guide keeps
GET/POST /rest/audio/mutedseparate from configuration.
Learn more: Modelling a device as resources and Optimistic concurrency with ETags.
Show answer
PUT for absolute state, POST for actions, and an idempotency key on every POST you can't undo. "Volume 40 and mute are PUTs: they say what the state is, so a retry is harmless. Volume up is a relative POST; a lost response plus a retry adds 20 instead of 10, so I keep relative actions for interactive remotes only. Placing a call and rebooting are POSTs that aren't idempotent, so they carry an Idempotency-Key and the device replays the first answer. Hanging up is DELETE /calls/{id}, and a second DELETE that gets 404 counts as done."
- RFC 9110 defines
GET,PUTandDELETEas idempotent and says clients shouldn't automatically retry non-idempotent methods, so the method choice is the retry policy. - On devices lost responses are routine: Wi-Fi drops, proxies time out, the device reboots itself. Design so the second copy of every request is harmless.
- The chapter's model shows it: a reboot with no key scheduled two reboots; with
Idempotency-Key: 7c1ethe retry replayed{"operation":"op-3","replayed":true};POST volume-upended at 60 when one press meant 50. - The
Idempotency-Keyheader is still an IETF draft (draft-ietf-httpapi-idempotency-key-header-08, 22 October 2026), not an RFC; it suggests400for a missing key,422for a key reused with a different body and409while the first request is still running. - Device twist: the key store must survive the reboot the request caused, so keep it in flash with the operation record, and keep keys for minutes to hours, not a payment API's day.
Learn more: Methods, status codes and idempotency for device APIs.
POST /firmware-updates can correctly return 503, 409 or 202. When is each right?Show answer
It depends on the device's state, which is why partners need these codes documented and stable. "503 with Retry-After while the device is booting: the HTTP server is up but the services behind it aren't. 409 Conflict while it's in a call: the request is valid but conflicts with the current state, and the client can fix that by waiting or scheduling. 202 Accepted with a Location for the operation once it's idle. Not 423 Locked, which is WebDAV's lock model, and not 500, which sends clients into error handling instead of waiting."
- Booting: a middleware in front of every route answers
503+Retry-After: 5. RFC 9110 definesRetry-Afterfor exactly "how long the service is expected to be unavailable". - In a call:
409is "a conflict with the current state of the target resource" that "the user might be able to resolve". Add a stable problemtypesuch as…/device-busyso a fleet tool can reschedule automatically. - Idle:
202plus an operation resource, because the update takes minutes and outlives the connection. - Others worth knowing:
405+Allowfor a method a resource doesn't support (Express 5 needs an.all()fallback to produce it),412/428for preconditions,413for oversized bodies,415for the wrong patch media type,422for well-formed but invalid values,429+Retry-Afterfor rate limits. - If you'd rather queue the update until the call ends,
202is a valid design, but say so in the operation's status instead of silently delaying.
Learn more: Methods, status codes and idempotency for device APIs.
Show answer
PATCH with JSON Merge Patch (RFC 7396, application/merge-patch+json), because an old client's PUT silently deletes settings it doesn't know about. "A management tool built for firmware 4.2 maps the response into its own model, drops the new autoWake field, and its PUT stores the device without it: nobody asked to turn it off, nothing errors. With merge patch it sends only { "display": { "brightness": 60 } } and everything else is left alone. On the server I parse that media type explicitly, return 415 with Accept-Patch for anything else, apply the patch to a copy, validate the result, strict about unknown keys, and swap it in atomically, as RFC 5789 requires."
- RFC 5789 says the server "MUST apply the entire set of changes atomically"; patch-a-copy, validate, swap gives you that for free.
- Express's JSON parser only parses
application/jsonby default, so a correct merge patch arrives withreq.bodyundefined unless you passtype: "application/merge-patch+json". The testing chapter's integration test catches exactly this bug returning200for a change that never happened. - Merge patch semantics: a member replaces,
nulldeletes, arrays are replaced whole, and a non-object patch replaces the whole document. - If you must keep
PUT, the safe options are all harder: treat missing members as unchanged, require clients to round-trip unknown members, or requireIf-Matchplus a declared schema version. - Concurrency is separate: merge patch alone doesn't stop two admins overwriting each other's array edits;
If-Matchdoes.
Learn more: Partial updates: PATCH and JSON Merge Patch.
Show answer
It can't set a value to null, can't touch one element of an array, and can't say "only if the current value is X". "For most device settings that's fine. Lists that people edit one item at a time, like directory entries or provisioning servers, become collections with their own ids instead of arrays inside the settings, which removes the main reason for anything else. If I genuinely need element-level or conditional edits, I add JSON Patch, RFC 6902, application/json-patch+json, alongside it: a list of operations addressed by JSON Pointer, with a test operation as a precondition, applied all or nothing."
- In merge patch,
nullalways means "remove this member", so nullable fields can't be set tonull. - Adding one NTP server means sending the whole new list built from your earlier read, which is a lost-update risk if two admins do it at once.
- JSON Patch's
testis field-level optimistic concurrency: "remove element 0 only if it's stillntp1.example.com"; if it fails, the whole patch is discarded (RFC 6902 section 5). - Pointer escaping is a classic client bug: a key like
room/flooris addressed as/labels/room~1floor(~1for/,~0for~, RFC 6901). - Whichever format, the server validates the merged result, not the patch: removals of required fields, a non-object patch and cross-field rules are only visible in the result.
Learn more: Partial updates: PATCH and JSON Merge Patch.
Show answer
POST /firmware-updates returns 202 Accepted at once, with a Location pointing at an operation resource that survives the reboot the update ends with. "The operation has a small status clients branch on (queued, running, succeeded, failed, cancelled), a finer step the UI shows (downloading, verifying, installing, rebooting), real progress only where something is counted, a cancellable flag, and on failure a problem-details error with a stable type like signature-invalid. Clients poll at the interval in Retry-After or listen to an event stream with polling as the fallback. A second update while one runs gets 409 with a link to the running one, and cancel is a POST to the operation that returns 409 once the flash write has started."
- RFC 9110:
202means "accepted for processing, but the processing has not been completed", and the response should point to a status monitor. Google's AIP-151 formalises the same pattern and suggests about 10 seconds as the threshold for needing one. - Separating
statusfromsteplets later firmware add steps without breaking clients that only checkstatus. Retry-Afteron a running operation tells a hundred browser tabs how often polling is useful.- Cancel is
POST /operations/{id}/cancel, notDELETE, because you want to keep the record of a cancelled update. - The UI trap:
202is "accepted", not "installed"; the image can still fail verification minutes later, so show "Update started" and follow the operation to its final state.
Learn more: Long-running operations.
POST /system/reboot to block until the device is back and then return 200. What goes wrong?Show answer
The process that would send the 200 is the one being restarted, so the answer has nowhere to go. "Its sockets close with it, the new process after boot knows nothing about that connection, and long before a minute-long reboot finishes, proxies, browsers and SDK timeouts give up anyway; a client that sees an error often retries, which is a second reboot. So the device answers 202 with an operation id, makes sure the response is written before it shuts down, keeps the operation record in flash, and the next boot marks it succeeded before the API opens. Clients treat connection refused and 503 during that window as 'in progress', not as failure."
- Send the
202and wait for the response'sfinishevent before starting the shutdown; otherwise the client gets a network error for a request that succeeded. - The operation record lives in storage that survives the reboot; on boot, an operation started in an earlier boot is marked done. That's the only way a reboot operation can succeed.
- In the chapter's model the polls went:
ECONNREFUSED(nothing listening),503 Device is starting(HTTP up before the API), then200 succeeded/done. - Real clients wait up to a documented maximum (say ten minutes for an update) before declaring the device lost.
- Poly's public guide documents
POST /rest/system/rebootas returning200 OKwith no body: the older, simpler style, where the client learns about the reboot by the device going quiet and coming back.
Learn more: Long-running operations and, for Poly's own reboot call, How a web UI, device APIs and back-end services fit together.
Show answer
Without protection the last write wins and the first admin's change silently disappears; I'd use optimistic concurrency with a strong ETag and a required If-Match. "Every GET returns an ETag; every write must send it back in If-Match. The device compares before doing anything: a match applies the change and returns the new tag; a mismatch returns 412 Precondition Failed with the current tag, and the client re-reads, re-applies its own intent and retries. I'd require the header, answering 428 Precondition Required to writes without it, so no integration can bypass the check, and give each settings section its own tag so an audio change doesn't conflict with a network change."
- The lost update happens even with merge patch, because arrays are replaced whole and both clients compute their new list from what they read.
- Optimistic means nobody holds a lock: a lock held by a cloud service that then loses its network would block the local admin; an ETag check costs one string comparison.
- In the chapter's model both admins added an NTP server; without preconditions the UI's server vanished, with
If-Matchthe fleet service got412, re-read"r2", retried and both servers survived. - Return the new ETag on every successful write so the next change needs no
GET. - Status doesn't need
If-Match(nobody writes it), but its ETag still makesIf-None-Matchpolling cheap.
Learn more: Optimistic concurrency with ETags.
Show answer
Two different ETag bugs: Express's automatic tags are weak, and a revision counter that restarts on reset repeats old tags. "RFC 9110 says If-Match must use the strong comparison, and under strong comparison a weak tag like W/"1e-…" never matches anything, not even itself, so every write fails and someone eventually turns the check off. The fix is to set your own strong ETag on writable resources. The second bug is ABA: after a factory reset the counter is back at "r2" for a different document, so a script's old If-Match: "r2" wrongly matches. Derive the tag from a hash of the canonical content, or add a reset epoch so a tag never comes back."
- Express adds a weak ETag to every
res.json()body by default; that's fine forIf-None-Matchcaching, which uses weak comparison. - Setting
res.set("ETag", …)beforeres.json()makes Express keep your tag instead of generating its own. - In the chapter's model the counter tag
"r2"matched after a reset (wrongly accepted), while the SHA-256 content tag returned412correctly. - Hash a canonical serialisation (sorted keys), or keep a counter outside the area a factory reset wipes, or combine it with an epoch:
"e3-r2". - Rule: the tag must change whenever the representation changes and never come back for a different one.
Learn more: Optimistic concurrency with ETags.
Show answer
RFC 9457 problem details everywhere, with the type URI as the contract and everything else as the message. "Every error is application/problem+json with a type that identifies the kind of problem and never changes, a title for that type, the status, a detail for this occurrence that clients must not parse, and an instance. Machine-readable specifics go in extension members, like an errors array of JSON Pointers for validation or activeCall for a busy device, and clients ignore extensions they don't know. Generic cases use about:blank. A 500 never carries a stack trace, only an instance id support can find in the device logs. The types are documented in an error catalogue, and partners switch on type, falling back to the status code."
- RFC 9457 (July 2023) obsoletes RFC 7807.
title"SHOULD NOT change from occurrence to occurrence";statusis only advisory and must match the real one; clients "MUST ignore any such extensions that they don't recognize". - Types are forever: never rename or reuse one, use absolute URIs on a domain you'll keep, and localise
titleanddetail, nevertype. - If useful information only lives in
detail, someone will regex it, and your next copy-editing pass breaks their integration; put every value a program needs in an extension. - A good client also copes with errors that aren't problem details at all, like a proxy's HTML
502, by falling back to the status. - Extending an existing API: Poly's public guide uses
415for "Payload invalid or missing one or more required fields" and404for "Operation or source unavailable". Partners code against what a device actually returns, so add problem bodies without changing existing status codes, and change codes only in a new API version.
Learn more: Errors with problem details.
Show answer
Nothing at all: one request is one transaction. "The device checks every value, then runs cross-setting rules on the complete candidate configuration, such as 'SIP mode needs a SIP server', and only if everything passes does it swap the configuration in one step and bump the revision. Any error returns 422 with every problem and applied: false. I'd offer a dry run that runs the same code minus the final swap and returns a diff with each change marked live or restart-required, require If-Match with the configuration revision so a stale profile can't undo today's edits, and report restart-required settings as pending in /status instead of pretending they took effect."
- Applied one
PATCHat a time, a group passes through states nobody designed (SIP mode with no SIP server yet), and a failure halfway leaves the device half-configured with no record. - The dry run must be the same code path as the real apply; a separate validator drifts. Kubernetes' server-side
?dryRun=Allis the same idea. - Return a diff, not just "valid":
provider.mode: teams → sip (restart)is what reviewers and fleet tools need. - Network equipment has worked this way for years: NETCONF (RFC 6241) edits a candidate configuration, validates it and commits.
- Applying a profile to 500 rooms is a different, cloud-side job with partial success per device; on each device keep it all or nothing.
Learn more: Bulk and transactional configuration.
Show answer
Confirm or revert: settings that can cut the admin off are applied on probation, and the device itself rolls them back unless someone confirms from the new state. "That's NETCONF's confirmed commit: RFC 6241 says such a commit must be reverted if no confirming commit arrives within the timeout, 600 seconds by default, or if the device reboots first. So POST /config/changes with a confirm timeout arms a timer on the device, the UI polls the new address, and only a successful confirmation from there keeps the change. The timer has to live on the device; a timer in the browser is useless once the browser can't reach it."
- Validation proves an IP address is well-formed, not that the admin can still reach the device on it: wrong gateway, wrong VLAN, a certificate the browser won't accept, an HTTP port turned off.
- In the chapter's model the mistaken VLAN change rolled back by itself at 60 s and the admin could reach the device again; the planned move was confirmed from the new VLAN and kept.
- The confirm request is cross-origin: the UI was loaded from the old address, so the session cookie isn't sent to the new one. Return a one-time confirm token and allow exactly the old origin for that endpoint, or move the whole page to the new address.
- Apply it to network, ports, certificates and auth changes only; for everything else the extra minute of uncertainty isn't worth it.
- With it, a timed-out save of a static IP has an outcome that can't stay "unknown" for long.
Learn more: Bulk and transactional configuration and Apply, restart and rollback flows.
/status (2 KB) and /settings (8 KB) every 5 seconds, and five people leave it open. What does that cost the device, and what are the two biggest fixes?Show answer
About 36,000 KB of response bodies an hour and 7,200 serialisations, 80% of it settings that change twice a day. "Fix one: a strong ETag and If-None-Match on settings, with freshness checked before the document is built, so nearly every poll is a body-less 304 the device doesn't even render. Fix two: push status as events on one stream per tab, carrying only the fields that changed. Under the assumptions below, that's about 900 KB an hour instead of 36,000."
// An admin dashboard polls /status (2 KB) and /settings (8 KB) every 5 s; five tabs stay open all hour.
// Assumptions (stated, not measured): settings change twice a day; status changes on most polls, and an
// event carrying only the changed fields is about 0.25 KB. Response bodies only; headers left out.
const tabs = 5, intervalS = 5, statusKB = 2, settingsKB = 8, eventKB = 0.25, settingsChangesPerDay = 2;
const pollsPerTab = 3600 / intervalS; // polls of each resource, per tab, per hour
const kb = (n) => `${n.toLocaleString("en-US", { maximumFractionDigits: 1 })} KB`;
const plainSettings = tabs * pollsPerTab * settingsKB;
const plainStatus = tabs * pollsPerTab * statusKB;
console.log(`plain polling: settings ${kb(plainSettings)} + status ${kb(plainStatus)} = ${kb(plainSettings + plainStatus)}/hour`);
console.log(` requests the device serialises: ${(2 * tabs * pollsPerTab).toLocaleString("en-US")}/hour`);
// Fix 1: strong ETag + If-None-Match, freshness checked before rendering: a body only after a change.
const fix1Settings = tabs * (settingsChangesPerDay / 24) * settingsKB;
console.log(`fix 1, settings answered 304 unless changed: ${kb(fix1Settings)}/hour`);
// Fix 2: status pushed as one event stream per tab, only the fields that changed.
const fix2Status = tabs * pollsPerTab * eventKB;
console.log(`fix 2, status as events: ${kb(fix2Status)}/hour`);
const after = fix1Settings + fix2Status;
console.log(`after both: ${kb(after)}/hour, ${(100 * (1 - after / (plainSettings + plainStatus))).toFixed(1)}% less`);plain polling: settings 28,800 KB + status 7,200 KB = 36,000 KB/hour
requests the device serialises: 7,200/hour
fix 1, settings answered 304 unless changed: 3.3 KB/hour
fix 2, status as events: 900 KB/hour
after both: 903.3 KB/hour, 97.5% less- Each tab polls each resource
3600 / 5 = 720times an hour, so five tabs make 3,600 polls of each:3600 × 8 KB = 28,800 KBof settings and3600 × 2 KB = 7,200 KBof status, the same numbers as the chapter's quiz. - With conditional requests, a settings body is only sent after one of the two daily changes:
5 × (2/24) × 8 KB ≈ 3.3 KBan hour. The304s still cost headers and a request each, which this model leaves out; the chapter's model shows why you checkreq.freshbefore rendering, so the device doesn't serialise 30 documents to send 2. - Events replace full 2 KB documents with small deltas: at an assumed 0.25 KB per change,
3600 × 0.25 KB = 900 KB. The event size and change rate are assumptions; change them to your device's. - A trap that silently cancels fix one: setting
If-None-Matchyourself onfetchwith the default cache mode makes the Fetch standard addCache-Control: no-cache, and Express then always sends a full200. Usecache: "no-cache"on hand-written conditional requests. - Then lengthen the interval for anything not on screen; a hidden tab can poll once a minute or stop.
Learn more: Designing for constrained devices and Polling, long polling, SSE and WebSockets for device status.
Show answer
It can't scale out, and it shares the CPU with the device's real job, video and audio, so the API has to be cheap by design and bounded everywhere. "Polling is made cheap with strong ETags and freshness checked before rendering; live status is pushed as events rather than polled; responses are small, with settings in sections, ?fields=, paginated lists with a hard maximum and large files streamed. Every request is bounded: per-route body limits that return 413, per-client rate limits that return 429 with Retry-After, one expensive operation at a time, a connection cap and short timeouts. And the device tells well-behaved clients how to back off, with Retry-After while booting and on running operations. Then measure on the real hardware."
- Reject by rate before parsing the body, so a flood of large requests costs almost nothing.
- Express's JSON parser defaults to
100kb; set limits per route, and give firmware uploads their own streaming endpoint. Poly's public REST guide documents a 10 MB cap for its API. - Node 26's HTTP server defaults include a 5-minute
requestTimeoutand no connection cap; a device wants shorter timeouts on ordinary routes. - A raw
server.maxConnectionscap refuses clients with a reset, not a helpful503, and can lock out the admin with the dashboard; combine a generous cap with limits on expensive operations. - Compression costs CPU: often better to pre-compress large cacheable downloads and leave small API responses alone. Measure on the device.
Learn more: Designing for constrained devices.
Show answer
Browser, device web server, auth and validation, settings service, platform service, and back, with nothing reporting "done" before the platform confirms. "The browser sends a PATCH with only the changed fields; the device's own web server terminates TLS; the API layer checks the session cookie and XSRF header and validates the body against the schema, so bad requests stop before anything changes. The settings service stages the change and calls the platform service that owns the resource, on Android usually a system service over Binder, and waits. On success it commits a new config version, publishes a settings-changed event to every subscriber, and the API returns 200 with the version and a restart-required flag. On failure it rolls back, and the API maps the platform's error to a stable one, like 503 with Retry-After for 'busy'."
- Public Poly documents don't describe their devices' internals, so this is the general design, not PolyOS.
- The slow, failure-prone part is almost always the platform call, not the web stack (120 of the model's 129 fake milliseconds).
- The ordering bug to avoid: bumping the version and sending the event before the platform call, so a failed save announces "Wi-Fi on" to every screen while the radio stays off.
- Error codes are translated in one place, the API layer, so a platform refactor can't change what partners see.
- Slow operations (reboots, updates, log bundles) return
202with an operation instead of holding the request open.
Learn more: How a web UI, device APIs and back-end services fit together and Anatomy of a managed device.
Show answer
In the settings layer's schema, so every writer is refused with the same error. "The value got in through the CLI, but patching the CLI parser leaves REST and provisioning open, and truncating the header in the UI hides the symptom for one viewer. I'd add maxLength to device.local.deviceName in the one schema every front door calls, so CLI, REST and provisioning all get INVALID_VALUE, and the UI reads the same limit before the user presses Save. Then the device-specific part: devices in the field may already hold a long name, so the release needs a migration that meets that old data on first boot."
- The architecture point: one schema and one command layer behind every front door, because separate validators per door drift apart over years of releases.
- In the chapter's model, the CLI write and the REST read went through the same
writefunction and landed in the same layer: two doors, one truth. - The config store has layers (defaults, local, provisioned that win), and each read reports its
source, so the UI can say "managed centrally". - Fixing only the UI is the "two front doors, two validators" bug in reverse.
- A new rule always meets old data on devices: plan the truncate-or-flag migration in the same change.
Learn more: Anatomy of a managed device and Validation shared between UI and device.
Show answer
A machine-readable description of an HTTP API, and in 3.1 its schemas are real JSON Schema 2020-12. "Paths, operations, parameters, request and response bodies, security schemes, and in 3.1 a root webhooks map for requests the device sends out. Because the file is data, it generates docs and types, validates traffic at runtime and gets diffed for breaking changes. The changes that bite: nullable is gone in favour of type: [string, "null"], exclusiveMinimum is a number instead of a boolean, schema example became examples arrays. And openapi: 3.1.1 is the spec version, info.version is your API's version; mixing them up is a classic slip."
- Dates from the specification's revision history (checked 27 October 2026): 3.0.0 July 2017, 3.1.0 February 2021, 3.2.0 September 2025 with a 3.2.1 patch in September 2026.
- Tools disagree about 3.0 habits in a 3.1 file: in the chapter's model Ajv and openapi-typescript both accepted
nullable: true, while Redocly's linter rejected it. A strict validator in another language might reject everynull. - A 3.0
{ minimum: 0, exclusiveMinimum: true }makes Ajv in 2020-12 mode refuse to compile the schema: "data/exclusiveMinimum must be number". Good: a loud failure at startup. - Why 3.1 and not 3.2 today: swagger-parser 13.1 rejects 3.2 documents, so move when the validator, type generator and docs tool all support it.
- A file starting
swagger: "2.0"is the older format; convert it once rather than maintaining both.
Learn more: OpenAPI 3.1 in an interview.
Show answer
Shared components for every shape, every operation with its error responses, and CI that checks the file in three ways. "One schema per resource, one Problem schema for every error as application/problem+json, reusable responses and the If-Match header defined once. Every operation gets a unique operationId, a summary, tags and its 4XX responses; partial updates are application/merge-patch+json with a patch schema that has additionalProperties: false; reboots are 202 plus an operation; the server URL is a variable because every device is its own host. In CI, swagger-parser proves it resolves, a linter like Redocly or Spectral proves it makes sense, and a script validates every example against its own schema, because examples are what partners copy."
- swagger-parser and a linter catch different things: in the chapter's model a broken
$reffailed both, but a duplicateoperationId, an undeclared path parameter andtype: integrpassed swagger-parser and only Redocly caught them. - Validators are software with versions: swagger-parser 13.1 rejected a server-variable
descriptionbecause its bundled schema iteration misspelled the property. Pin tools and know their bugs. - Examples drift:
copiedFromOldGuidefailed with"62"where an integer was required. - Writing a spec from a PDF reference guide surfaces its gaps: the linter's
security-definederror showed that Poly's public guide never says how the session id travels on later requests. - Name schemas and
operationIds for the long run: they become type and method names in generated code, so renaming them breaks SDK users even when the wire format is identical.
Learn more: Writing a spec for a device API.
Show answer
Make the spec executable: compile its schemas at startup and check every request before the handler and every response after it. "Middleware validates the media type, parameters and body against the operation in the spec and turns failures into problem details with JSON Pointers to the bad fields: 415 for a media type the spec doesn't declare, 400 only for a body that can't be parsed, 422 for well-formed JSON that breaks the schema, like volume 150. Handlers only ever see valid data. Responses are validated against the schema for their status code: in tests and staging a violation fails the request, so the first test that hits a contract bug says why; in production I'd log violations, or sample, rather than turn a shipped contract bug into an outage. Compile once, validate many."
- In the chapter's model a native layer returned
muted: 0; the response validator caught "data/muted must be boolean" before a body that would break every typed client went out. - A valid body sent as
application/jsonto an operation that only declaresapplication/merge-patch+jsongets415: the media type is how the client says which semantics it means. Fix it in the client or the spec, never with an undeclared exception in code. - Query and path parameters arrive as strings, so they need coercion; in Express 5
req.queryis a getter that re-parses the URL on every read and can't be assigned, so copy the coerced values (tores.locals) instead of mutating it. format: date-timeisn't checked unless you add formats; Ajv's strict mode refuses to compile an unknown format rather than pretend.coerceTypes,removeAdditionalanduseDefaultsmutate data: fine for parameters, but on bodies they hide client bugs a stricter device would reject.
Learn more: Validating requests and responses against the spec.
Show answer
Types, a thin client, docs and mocks, but never values: types check shapes, the device still validates. "openapi-typescript turns the spec into TypeScript types, so a wrong field name, a string where a number belongs or an enum value that doesn't exist is a compile error in the web UI. A thin typed client like openapi-fetch sits on top, or a generated SDK for partners in other languages; Redoc or Swagger UI render the docs and a mock server serves the examples so the UI team can start early. What the types lose: integer becomes number, and minimum, maximum, pattern, minProperties and additionalProperties vanish, so { volume: 150 } compiles and the device rejects it at runtime."
- In the chapter's type-check model,
volume: "70"(TS2322),speaker: "BOTH"(TS2322) andbalance: 0(TS2353) failed to compile;{}and{ volume: 150 }compiled. - The excess-property check only applies to object literals: a patch built elsewhere with extra fields compiles, and the device rejects it.
- Generated files are never edited by hand: CI regenerates them and fails on a
git diff. - Docs belong on a developer portal, not on a constrained device; the device serves its spec as JSON for discovery.
- Commit generated SDKs partners download (they need a changelog); regenerate types used only inside one repo in the build.
Learn more: Generating types, clients and docs.
Show answer
Design-first for anything partners and other teams consume, code-first for internal endpoints that ship in lockstep with their only consumer. "The spec pull request is an asynchronous design review: a developer in India proposes an endpoint as a YAML change at the end of their day, I review the shape while they sleep, and the UI team starts against a mock the next morning, with no meeting. Its risk is drift, so the spec is enforced: runtime validation, examples validated in CI, a breaking-change diff on every PR. Code-first with something like Zod is fine for internal endpoints, as long as the generated spec is reviewed and diffed too, because its defaults can quietly turn into partner bugs."
- Design-first fails by drift unless enforced; code-first fails by accident, when a refactor changes a public contract nobody knew was public.
- The chapter's Zod model found two real differences:
z.toJSONSchemaemittedadditionalProperties: falseon the response schema (a strict client then breaks the day the device adds a field), and a.refinefor "at least one field" vanished. - The spec PR is the cheapest place to change your mind: a rename costs one comment there and a deprecation cycle after the UI, device and a partner shipped against it.
- A common hybrid: design-first for the public surface, code-first internally, both through the same lint and diff steps.
- A review checklist keeps async review consistent: naming, declared errors, patch semantics, pagination, limits, compatibility with older firmware.
Learn more: Design-first vs code-first.
muted to isMuted, make firmwareVersion optional in a response, narrow volume to 0–80 in a request, add an optional ?since= query parameter?Show answer
Only the optional query parameter. "The rename is a removal plus an addition, and removing a field clients read is breaking; the safe way is to add isMuted, keep muted, and deprecate it. Making a response field optional is breaking, because clients that assumed it's always there now need a null check. Narrowing a request range breaks clients that sent 90 yesterday. A new optional query parameter is additive as long as the behaviour without it is unchanged. Direction decides most verdicts: what's safe in a response can break in a request, and the other way round."
- Request direction: adding a required field, making an optional one required, narrowing a range or removing an enum value is breaking; widening is safe.
- Response direction: removing a field, changing its type, or making a required field optional is breaking; adding a field is safe for tolerant readers; adding an enum value is risky for clients with exhaustive switches.
- No diff tool can tell a rename from a removal, which is why renames are always add-and-deprecate.
- Changing a parameter's
defaultchanges no schema shape, so naive diffs miss it, yet every client that relied on the default sees different behaviour. - A schema diff can't see behaviour changes (a reboot that now waits for the call to end), so "0 breaking changes" is necessary, not sufficient.
Learn more: Contract testing and Rules for compatible change.
Show answer
Consumer-driven contracts: each consumer records the calls and fields it really uses, and the provider replays every consumer's list against the new build before it ships. "In the chapter's model, renaming muted to isMuted broke the web admin UI's two interactions, while the partner room panel and the fleet manager passed, because they never read that field. That turns a yes/no gate into a decision with names attached: the web UI ships in the same firmware image and can change in the same release; had it broken the partner panel, it would need a deprecation period and a conversation. Pact automates this; for partners who won't run your tooling, ask for the endpoints and fields they use and keep that as a contract file."
- Provider conformance alone can't catch it: the 2.0.0 build matched its own new spec perfectly.
- Keep the released spec of every supported firmware line in the repo and diff against each, because a change additive to 5.0 can still break an integration written for 4.6.
- oasdiff's documented levels are
ERRfor definite breaks andWARNfor ones that can't be confirmed programmatically, with--fail-onfor CI. - A field that's in the spec but in no consumer contract is a candidate for deprecation, never for silent removal: partners who don't publish contracts may use it.
- Integrator-facing release notes cover the behaviour changes neither diff can see.
Learn more: Contract testing.
Show answer
It tells the truth about the device's state, makes changes safe, and keeps working while the device is only half alive. "I'd start from the operator's five jobs: is it healthy, change settings, diagnose, update, secure it, which gives a dashboard, settings, diagnostics, maintenance and security. The dashboard is computed from the device's status into a short list of problems with actions, worst first, not a wall of numbers. Wherever they can differ, it shows what's saved, what's active, and when it last heard from the device. And the UI is the API's first client: everything it does goes through the same public REST API scripts and partners use, so a setting only the UI can change is a setting nobody can automate."
- The mental model is a cockpit for a remote aircraft: the admin usually isn't in the room, people may be in a meeting there right now, and a wrong change can take the room offline with no way back in.
- In the chapter's model, a disconnected ceiling mic became the one critical item, a pending restart and an expiring certificate warnings, and an available update only "info": showing "available" in red trains admins to ignore red.
- Because the dashboard is a function of state, the same function can run over a thousand devices in a fleet view, and "a disconnected mic is critical" is a unit test.
- The most common failure is a UI that shows what the admin asked for instead of what the device is doing: "Teams" on the page while the old app still runs until a restart.
- Poly's public 4.7.0 guide says the web interface can configure every setting while the in-room interface is intended for initial setup, and that provisioned settings become read-only there.
Learn more: What device admin UIs must do well.
Show answer
Describe each setting once, as data, in a JSON Schema served by the device, and generate the form, both validators, the docs and the SDK types from it. "JSON Schema 2020-12 already has type, enum, ranges, patterns, defaults, titles, readOnly, writeOnly and if/then; I add annotation keywords for device facts it lacks, like x-restart and x-group. A generic renderer in React or Angular turns the generated field list into accessible fieldsets: the widget comes from the type, visibility from if/then, constraints become input hints. Because each device serves its own schema, a UI talking to old firmware builds exactly the form that firmware supports."
- Four hand-written copies (form, browser validator, device validator, docs) drift; one blueprint, rebuilt, doesn't.
- The
if/thenrule "static IP needs an address and a prefix" drives both validation andshowWhen, so the UI can never hide a field the device requires. - A
writeOnlypassword becomes a password field and is left out of exports: one annotation, two behaviours. - Libraries exist (react-jsonschema-form, JSON Forms, ngx-formly), good for internal tools; your own renderer fits a strong design system and device-specific widgets. Either way the schema stays the source of truth.
- Decide early where presentation lives: meaning (type, limits, restart) in the settings schema partners read, order and help text in a separate UI schema.
Learn more: Forms generated from a settings schema.
Show answer
One schema, compiled in two places: in the browser for instant feedback, on the device as the authority, plus device-only checks the UI can't make. "The browser compiles with allErrors so the form can mark every bad field at once. The device compiles stopping at the first error, with a body-size limit, because its API is reachable by scripts that never saw the form, and Ajv's own security docs say 'Do NOT use allErrors in production'. Rules only the device knows, like the connected display's maximum resolution, come back as 422 problem details with a JSON Pointer, so the UI puts the message on the right field. And the device never trusts that the UI ran."
- Three layers: field rules and cross-field rules live in the schema and run on both sides; device-state rules live in device code.
- Ajv 8 is strict: an unknown keyword like
x-groupthrows until you register it withaddVocabulary. Don't turn strict mode off. - In the chapter's model, firmware 4.2.1's schema had no
4K, so the UI blocked it before sending; a script skipping the UI got the same422from the device. - Build user messages from Ajv's
keywordandparams, not its Englishmessage, so they can be translated and suggest a fix (WCAG 3.3.3). - Under a strict CSP Ajv can't compile at runtime (it uses
new Function), so compile standalone code at build time, bundle it, and import the bundle in CI.
Learn more: Validation shared between UI and device.
Show answer
Save the setting, don't restart, and say so. "'Saved. The new provider starts after a restart. This room is in a call: restart now, which ends the call, or later?' The batch goes to the device as one merge patch with If-Match; the device stores it, applies live settings at once and returns which settings are pending a restart. That list is device state, so the dashboard keeps showing 'Restart needed to apply: provider' to anyone who looks until it happens. Silently restarting would drop a live meeting; silently not restarting leaves the admin believing Teams is active."
- The restart flag comes from the schema (
x-restart), so the Save button becomes "Save and restart…" only when the pending batch includes one. - Edits changed back to their loaded value are dropped from the pending list, so the button doesn't claim a restart that isn't needed.
- "Later" can be a maintenance window; Poly's 4.7.0 guide documents a scheduled daily or weekly auto restart.
- On a
412because another admin saved first, rebase the pending edits three ways (baseline, mine, current) and show only real conflicts. - Collect, then restart once: five restart-required changes cost one restart.
Learn more: Apply, restart and rollback flows.
Show answer
Nothing has been installed yet, and the hardest steps haven't started. "After the upload the package still has to pass the checksum and the signature check, then install to the inactive slot, reboot, and pass a post-boot health check before the device marks the new version good. In the chapter's model, five of six scenarios ended badly after a 100% upload: two rejected, two rolled back, one not responding. So the UI shows each phase honestly: real percentages where something is counted, an indeterminate state during the reboot, and 'complete' only when the device reports it's running the new version after its check."
- The checksum catches accidents (a zeroed chunk); the signature against the vendor's public key catches forgeries with a matching self-computed checksum.
- On Android devices built with A/B updates,
update_enginewrites the unused slot and the new slot only counts as good oncemarkBootSuccessful()is called; the public Poly docs don't say which scheme PolyOS uses. - "Never comes back" after the deadline is "not responding, check the room", never "failed": the UI only knows it can't reach the device.
- Uploads are resumable: the device says how much it has (
Upload-Offset, from the IETF resumable-upload draft, still not an RFC) and the progress bar shows the device's count, not bytes sent. - Once uploaded, the device owns the rest; the UI is a window onto an operation resource, so closing the laptop doesn't break the update.
Learn more: Software update flows.
Show answer
Know which kind of silence it is, keep showing what it last knew, and never queue writes. "One failed poll is noise: keep the data, marked 'as of' the last success. A few in a row is unreachable: a status banner says so, writes are disabled with one disabled fieldset, unsaved edits are kept, and polling backs off to a cap. If the admin started a restart, silence is expected, so it says 'Restarting…', polls steadily and only escalates after a deadline based on the measured boot time. Every status reply carries a boot id: the same id means a network blip, a new one means the device restarted, so reload settings and version and expect to log in again."
- Don't queue writes for later: an admin who pressed Save and left didn't agree to it being applied an hour later in a different context.
- Don't retry a timed-out write blindly: re-read first, because you can't know whether it was applied (a static IP change may have succeeded and moved the device).
- Red "unreachable" during a restart the admin asked for is a success that looks like a failure, and it trains admins to ignore the real one.
- A DHCP device can come back on a different address; the "lost" screen shows hostname and MAC, and fleet tools avoid the problem because devices connect out to them.
navigator.onLineis about the admin's machine, never about the device.
Learn more: Device unreachable, rebooting and offline states and Reconnection and back-off.
Show answer
Export a versioned file that leaves secrets and per-device identity behind; import it through a pipeline that migrates, filters, validates and shows a dry-run diff before one atomic apply. "The export records its format, schema version, source model and firmware, and lists what it omitted: write-only secrets like the admin password, and per-device identity like the room name and IP, which would give fifty rooms the same address. Import migrates old files forward one version at a time, skips what the target model can't take with a reason, validates with the same schema as the forms, and shows only real changes. With a provisioning service, every setting has a source, and the device refuses local writes to provisioned ones with 409."
- An export file is a secret until proven otherwise: support tickets and shared folders leak them. A "full backup with secrets" is a separate, named, encrypted, audited action.
- Migrations are small pure functions, one per schema step, kept forever, so a 2024 export still imports.
- The dry run is the end state until the admin approves; the apply is the normal merge patch with
If-Matchand restart handling. - Poly's public 4.7.0 guide: "Provisioned settings are read-only in the system web interface", and a system that loses registration keeps the most recent configuration it received. A provisioning template can carry the admin password, so treat it like a secret too.
- The device, not just the greyed-out control, refuses the write, so a script gets the same answer as the form.
Learn more: Import, export and provisioning of configuration.
Show answer
WCAG 2.2 AA done consistently, and Intl plus translator-owned templates for everything locale-dependent. "Real labels on every field; errors as text on the field, linked with aria-describedby, with a suggested fix; nothing conveyed by colour alone; status changes like 'Saved', 'Restarting' and 'Device not responding' in a role="status" live region; visible focus that a sticky bar never covers; targets of at least 24 pixels; a login that works with password managers and paste; session timeouts that warn and can be extended. For localisation: dates, numbers, units, relative times and plurals through Intl, device times shown with the zone labelled, and the device returning error codes and parameters, not English sentences."
- WCAG 2.2 is a W3C Recommendation (republished 12 December 2024); 2.4.11 Focus Not Obscured, 2.5.8 Target Size and 3.3.8 Accessible Authentication are new in 2.2.
- Show both clocks: the room's (what people there remember) and the admin's (their calendar), with the zone in the string.
- Plurals differ by language: Polish needs
one,fewandmany, whichIntl.PluralRuleshandles andn === 1 ? … : …doesn't. - Use logical CSS properties for right-to-left, and
dir="ltr"on IP addresses, versions and log lines even inside Arabic text. - Test with pseudo-localised strings; short English labels can grow by 200–300% in translation.
Learn more: Accessibility and localisation for admin UIs.