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?

29.3.2 · L3 · Interview

Reported at: Razorpay

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. URLSession only throws for transport failures, not for a 404 or 500.
  • Hard-coding URLSession.shared and 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 Task and 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… →