All docs
4 min read

Mobile recipe

The endpoint is HTTPS POST. Anything that speaks JSON or multipart works. Snippets here for React Native and Swift; the pattern translates directly to Kotlin / Flutter / .NET MAUI.

React Native

JSON submission with retry on transient failure:

// useFormspring.ts
import { useState } from 'react';

const ENDPOINT = 'https://formspring.io/f/r2EdO-orF-3S';

export function useFormspring() {
  const [sending, setSending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function submit(payload: Record<string, unknown>) {
    setSending(true);
    setError(null);

    for (let attempt = 1; attempt <= 3; attempt++) {
      try {
        const r = await fetch(ENDPOINT, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
          body: JSON.stringify(payload),
        });

        if (r.ok) {
          setSending(false);
          return true;
        }
        if (r.status >= 400 && r.status < 500) {
          // client error — don't retry
          setError((await r.json())?.message ?? `HTTP ${r.status}`);
          setSending(false);
          return false;
        }
        // 5xx — fall through to retry
      } catch (e: any) {
        if (attempt === 3) {
          setError(e.message ?? 'Network error');
        }
      }
      await new Promise((r) => setTimeout(r, 500 * 2 ** (attempt - 1)));
    }

    setSending(false);
    return false;
  }

  return { submit, sending, error };
}

Multipart (file upload)

async function submitWithPhoto(uri: string, fields: Record<string, string>) {
  const fd = new FormData();
  Object.entries(fields).forEach(([k, v]) => fd.append(k, v));

  fd.append('photo', {
    uri,
    name: 'photo.jpg',
    type: 'image/jpeg',
  } as any);

  const r = await fetch(ENDPOINT, {
    method: 'POST',
    body: fd,
    // do NOT set Content-Type; React Native sets the multipart boundary
  });

  return r.ok;
}

The { uri, name, type } shape is the React Native convention — the runtime streams the file from disk without loading it into JS memory.

Swift (URLSession)

JSON submission:

import Foundation

struct Submission: Encodable {
    let email: String
    let message: String
}

enum FormspringError: Error { case http(Int), transport(Error) }

func submit(_ payload: Submission) async throws {
    let url = URL(string: "https://formspring.io/f/r2EdO-orF-3S")!
    var req = URLRequest(url: url)
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.setValue("application/json", forHTTPHeaderField: "Accept")
    req.httpBody = try JSONEncoder().encode(payload)

    let (_, resp) = try await URLSession.shared.data(for: req)
    guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
        throw FormspringError.http((resp as? HTTPURLResponse)?.statusCode ?? -1)
    }
}

Multipart in Swift

func submitMultipart(email: String, imageData: Data) async throws {
    let url = URL(string: "https://formspring.io/f/r2EdO-orF-3S")!
    let boundary = "Boundary-\(UUID().uuidString)"
    var req = URLRequest(url: url)
    req.httpMethod = "POST"
    req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    var body = Data()
    func append(_ s: String) { body.append(s.data(using: .utf8)!) }

    append("--\(boundary)\r\n")
    append("Content-Disposition: form-data; name=\"email\"\r\n\r\n")
    append("\(email)\r\n")

    append("--\(boundary)\r\n")
    append("Content-Disposition: form-data; name=\"photo\"; filename=\"photo.jpg\"\r\n")
    append("Content-Type: image/jpeg\r\n\r\n")
    body.append(imageData)
    append("\r\n--\(boundary)--\r\n")

    req.httpBody = body
    let (_, resp) = try await URLSession.shared.data(for: req)
    guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
        throw FormspringError.http((resp as? HTTPURLResponse)?.statusCode ?? -1)
    }
}

Network handling

Mobile networks fail in ways desktop ones don't — radio off, captive portals, walking out of range mid-request. Defensive defaults:

  • Timeout: 30 seconds for the connection. Don't block UI on it.
  • Retry: only on 5xx and transport errors. Never retry on 4xx — the request is bad and will keep being bad.
  • Backoff: exponential with jitter. 500ms, 1s, 2s.
  • Offline queue: store the payload locally, flush when reachability returns. Use NetInfo (React Native) or NWPathMonitor (Swift).

What's next