Machine-coding round: build a generic networking layer in about 45 minutes. What do you build, and how do you test it without a server?
Predict first
What does the client print against a stubbed server?
import Foundation
protocol Endpoint { var path: String { get }; var method: String { get }; var query: [String: String] { get } }
extension Endpoint { var method: String { "GET" }; var query: [String: String] { [:] } }
enum APIError: Error, Equatable { case badStatus(Int), decoding, transport }
struct APIClient {
let baseURL: URL
let session: URLSession
func send<T: Decodable>(_ endpoint: Endpoint, as type: T.Type = T.self) async throws -> T {
var comps = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: false)!
if !endpoint.query.isEmpty { comps.queryItems = endpoint.query.sorted { $0.key < $1.key }.map { URLQueryItem(name: $0.key, value: $0.value) } }
var req = URLRequest(url: comps.url!)
req.httpMethod = endpoint.method
let (data, response): (Data, URLResponse)
do { (data, response) = try await session.data(for: req) } catch { throw APIError.transport }
guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
throw APIError.badStatus((response as? HTTPURLResponse)?.statusCode ?? -1)
}
do { return try JSONDecoder().decode(T.self, from: data) } catch { throw APIError.decoding }
}
}
// Test seam: a URLProtocol that serves canned responses, so no network is needed.
final class Stub: URLProtocol, @unchecked Sendable {
nonisolated(unsafe) static var responder: ((URLRequest) -> (Int, Data))!
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
let (code, body) = Stub.responder(request)
let r = HTTPURLResponse(url: request.url!, statusCode: code, httpVersion: nil, headerFields: nil)!
client?.urlProtocol(self, didReceive: r, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: body)
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
struct Movie: Decodable, Equatable { let id: Int; let title: String }
struct Movies: Endpoint { let page: Int; var path: String { "movies" }; var query: [String: String] { ["page": "\(page)"] } }
let cfg = URLSessionConfiguration.ephemeral
cfg.protocolClasses = [Stub.self]
let client = APIClient(baseURL: URL(string: "https://api.example.test")!, session: URLSession(configuration: cfg))
Stub.responder = { req in print("GET", req.url!.absoluteString); return (200, Data(#"[{"id":1,"title":"Alien"}]"#.utf8)) }
let movies = try await client.send(Movies(page: 2), as: [Movie].self)
print(movies.map(\.title))
Stub.responder = { _ in (404, Data()) }
do { _ = try await client.send(Movies(page: 1), as: [Movie].self) } catch { print("error:", error) }
Stub.responder = { _ in (200, Data("not json".utf8)) }
do { _ = try await client.send(Movies(page: 1), as: [Movie].self) } catch { print("error:", error) }
- GET https://api.example.test/movies?page=2 / ["Alien"] / error: badStatus(404) / error: decoding
- ["Alien"] / error: transport / error: decoding
- GET .../movies?page=2 / ["Alien"] / error: decoding / error: decoding
Mental model. A post office counter: you hand over a standard form (the endpoint), the clerk handles stamps and delivery, and you get either the parcel decoded for you or a clear reason it failed.
Reported for Razorpay (anonymous, unverified), and a fair test of design sense. Build four small pieces. An Endpoint protocol (path, method, query, body) with default values; a client that turns an endpoint into a URLRequest, calls session.data(for:), checks the status code, and decodes with JSONDecoder into any Decodable type; a small typed error (badStatus, decoding, transport) so callers can react differently; and injection of the session and base URL. Keep it async throws and generic: send<T: Decodable>(_ endpoint:, as:). Use URLComponents to add query items (it escapes values) and sort them for stable URLs.
Test without a server by registering a URLProtocol subclass on an ephemeral URLSessionConfiguration that returns canned responses, which is what the check does: a 200 with JSON decodes, a 404 gives badStatus(404), and a 200 with garbage gives decoding. Then say what you would add with more time: auth header injection and token refresh on a 401, retry with backoff for idempotent requests, request cancellation, logging, caching and pagination. Do not forget that URLSession does not treat a 4xx or 5xx as an error; you must check the status code yourself.
The query item is added, a 200 decodes, a 404 becomes badStatus(404), and garbage JSON becomes a decoding error.
Common traps
- Treating any response as success.
URLSessiononly throws for transport failures, not for a 404 or 500. - Hard-coding
URLSession.sharedand the base URL. It cannot be tested or pointed at staging.
They might follow up with
- How do you add auth? A request adapter that adds the header, and on a 401 refresh the token once and retry the request.
- How do you cancel a search request? Keep the
Taskand cancel it;session.data(for:)stops when the task is cancelled.
Related questions
- 29.3.1 You get 60 to 90 minutes to build a small app live (a photo search, a movie list from JSON, a recipe app, a…
- 29.3.3 In a live round you are given existing code to review and then asked to add a feature to it. How do you…
- 29.2.2 Implement a hash map from scratch (get, set, remove), then extend it: what do you test and how does resizing…
← You get 60 to 90 minutes to build a small app live (a photo search, a… In a live round you are given existing code to review and then asked… →