File Transfer API Design: RESTful Best Practices
Design file transfer APIs following RESTful best practices. Authentication, rate limiting, multipart uploads, and error handling.
A well-designed file transfer REST API exposes five or six resources (sessions, parts, shares, downloads, revocations), uses HTTP semantics honestly (POST to create, PUT for idempotent chunks, DELETE for revocation), and pushes the actual byte movement to presigned storage URLs so your server never becomes the bandwidth bottleneck. Authentication uses short-lived bearer tokens over TLS 1.3, rate limits differentiate creation from metadata reads, chunked uploads follow the tus resumable pattern or S3 multipart semantics, and errors follow RFC 7807 Problem Details so clients can act on them programmatically.
Shape the Resource Model Around Actions, Not Files
A common mistake is modeling the API as a file tree. File transfer services are better modeled as three resources:
/sessions— an in-flight upload, created by POST, populated via chunk PUTs/shares— a completed, addressable transfer with an expiry and download budget/downloads— short-lived, signed access handles for fetching bytes
Sessions become shares via a complete action; shares become expired via time or download budget. Revocations are PATCH on a share or DELETE with a revocation token. No nouns for "file" or "folder" — those are implementation details of storage, not your public contract.
This shape keeps the API surface small (under 10 endpoints), maps cleanly to HTTP caching (shares are Cache-Control: private, max-age=60; downloads are no-store), and lets you swap storage backends without breaking clients.
Authentication and Authorization Patterns
For user-facing APIs, issue short-lived JWTs (15-minute TTL) signed with EdDSA, and refresh via a secure HttpOnly cookie. Put the token in Authorization: Bearer, never in query strings where it gets logged.
For machine-to-machine, HMAC-SHA256 request signing beats bearer tokens because it proves possession of a key without sending it. AWS SigV4 is the reference design:
Authorization: HEX4-HMAC-SHA256
Credential=AKIA.../20261202/eu/transfer/hex4_request,
SignedHeaders=host;x-hex-date;x-hex-content-sha256,
Signature=...
For presigned download URLs, TTLs should be minutes, not hours. A 15-minute window balances usability against the risk of a leaked URL being forwarded. Include an IP lock only if you can tolerate breaking users behind CGNAT — usually you can't.
Chunked Uploads and Multipart Semantics
Two credible protocols exist for resumable chunked uploads: tus (IETF draft, Upload-Offset and Upload-Length headers) and S3 multipart (PartNumber, UploadId, ETag). Pick one and commit. Inventing your own protocol looks tempting and ends badly when you realize you need to handle partial writes, out-of-order chunks, and abandoned sessions.
The tus pattern in REST form:
POST /sessions -> 201, Location: /sessions/abc
HEAD /sessions/abc -> 200, Upload-Offset: 104857600
PATCH /sessions/abc -> 204, body = next chunk
POST /sessions/abc/complete -> 201, Location: /shares/xyz
DELETE /sessions/abc -> 204
Chunk size bounds: 5 MB minimum to match S3 multipart, 100 MB maximum to keep retry cost bounded, default 8 MB. Reject chunks that don't align with the advertised offset with HTTP 409 Conflict plus a Problem document explaining the expected offset.
Rate Limiting That Reflects Real Abuse
Rate limits need to vary by endpoint cost:
POST /sessions: 20 per IP per hour (expensive — allocates storage)PATCH /sessions/:id: 10,000 per hour per session (cheap — writes bytes)GET /shares/:id: 1,000 per IP per hour (metadata lookup)GET /shares/:id/download: 100 per IP per hour (egress cost)
Apply limits at the edge (Cloudflare, Fastly) for floor protection, and a second layer at the application (express-rate-limit, Fastify's @fastify/rate-limit) for burst control. Return 429 with Retry-After in seconds. Include rate limit headers on successful responses too:
RateLimit-Limit: 20
RateLimit-Remaining: 17
RateLimit-Reset: 2843
Following the IETF RateLimit draft lets well-behaved clients pace themselves instead of being blocked.
Error Responses That Clients Can Act On
RFC 7807 Problem Details is the standard. Every error returns Content-Type: application/problem+json:
{
"type": "https://api.example.com/errors/chunk-offset-mismatch",
"title": "Chunk offset mismatch",
"status": 409,
"detail": "Server expected offset 5242880, received 4194304.",
"expected_offset": 5242880,
"session_id": "abc123"
}
The type URI should be documented and stable; that's what clients pattern-match on. title is generic; detail is specific. Custom fields add machine-actionable context. Never leak stack traces, file paths, or internal IDs in errors.
Map HTTP status codes honestly: 400 for malformed input, 401 for missing auth, 403 for authenticated-but-unauthorized, 404 only when the resource never existed (use 410 Gone for expired shares), 413 for over-quota payloads, 429 for rate limits, 500 for bugs, 503 for maintenance.
Content Negotiation and Streaming
Upload endpoints should accept application/octet-stream and require Content-Length. Reject multipart/form-data for chunk uploads — it adds parsing overhead and doesn't help anyone. Accept Content-Range for tus-style partial writes.
Download endpoints should support HTTP Range requests (RFC 7233) for resumable downloads:
GET /shares/xyz/blob
Range: bytes=104857600-209715199
-> 206 Partial Content
Content-Range: bytes 104857600-209715199/2147483648
This is what lets browsers resume a 2 GB download after a Wi-Fi hiccup. Most S3-compatible stores serve Range requests natively — your API just has to forward or presign.
Versioning Without Accumulating Debt
Version via the URL path (/v1/sessions) not headers. Path versioning is visible in logs, cacheable, and easier to debug than Accept: application/vnd.example.v1+json. Keep v1 supported for at least 24 months after v2 ships. Add fields freely (clients must ignore unknown fields); never remove or rename fields in a stable version.
When breaking changes are necessary, run v1 and v2 in parallel for 12 months, surface a Sunset header on v1 responses per RFC 8594, and publish migration guides with real before/after examples.
Observability and Debuggability
Every response should include a correlation ID (X-Request-ID) echoed from the request or generated. Log the ID, client IP (hashed if privacy-sensitive), endpoint, status, and duration in structured JSON. Don't log request bodies — that's how encryption keys end up in Datadog.
Emit metrics per endpoint: request count, p50/p95/p99 latency, error rate, bytes in, bytes out. Alert on p99 latency spikes and 5xx rate above baseline. Traces via OpenTelemetry give you request flow across API, storage, and database.
HexaTransfer's API follows the patterns above — short resources, tus-style chunk uploads, Problem Details errors, 15-minute presigned downloads, RateLimit headers. Try it at hexatransfer.com — free, no account, 10 GB max.
Documentation That Matches Reality
Publish an OpenAPI 3.1 spec alongside the API and keep it in the same repo as the server code so schema drift is a PR review issue rather than a production surprise. Generate at least one SDK (TypeScript or Python) from the spec and use it in your own examples — SDK bugs surface spec bugs fast. Include curl examples for every endpoint, a quickstart that uploads a real file in under 20 lines, and a page specifically documenting every error type URI. An API that developers can integrate in an afternoon will ship to 10x more products than one that requires a week of reverse engineering.
Send large files securely with end-to-end encryption
Transfer files up to 10 GB for free with end-to-end encryption. No account required. Your files are encrypted in your browser before upload — no one else can read them.
Send a file