The usual way to send a binary file alongside structured data in a REST API is to Base64-encode it and inline it in the JSON body. That works, but it comes at a cost:
- ~33% size inflation on every payload from Base64 encoding.
- Full buffering — the whole attachment has to sit in memory to be encoded and decoded, which doesn't scale to large files.
- No streaming — you can't start processing bytes until the entire blob has been received and decoded.
The SOAP world solved this years ago with MTOM/XOP: keep the structured message intact, but pull binary content out into separate, streamable MIME parts referenced by ID. RestXOP brings that same idea to plain REST.
RestXOP sends a multipart/related request whose root part is JSON. Binary attachments travel as separate parts, referenced from the JSON by content ID — so they stream, unencoded, instead of bloating the body.
POST /documents
Content-Type: multipart/related; type="application/json"; boundary=xop
--xop
Content-Type: application/json
{ "title": "Q3 Report", "file": { "$ref": "cid:doc-1" } }
--xop
Content-Type: application/pdf
Content-ID: <doc-1>
Content-Transfer-Encoding: binary
<raw PDF bytes, streamed>
--xop--
The JSON stays clean and readable; the bytes stay raw and streamable. Consumers that don't need the attachment can process the JSON root part without ever touching the binary.
- No Base64 bloat — binary parts are sent raw, streamed end to end.
- JSON-first — the message stays ordinary JSON, with lightweight
cid:references to attachments. - Java / Spring Boot server support — drop-in integration for producing and consuming RestXOP payloads.
- JavaScript / TypeScript clients — first-class support for browser and Node consumers.
- Memory-efficient — designed for large attachments without buffering the whole payload.
- Apache-2.0 licensed — open and free to use.
Both sides live in the main repository — restxop/restxop — and are published to Maven Central and npm.
Add the starter for your platform generation alongside your usual web starter:
<dependency>
<groupId>dev.restxop</groupId>
<artifactId>restxop-spring-boot-4-starter</artifactId> <!-- or -3-starter for Boot 3.5+ -->
<version>0.1.0</version>
</dependency>Give a payload class an Attachment field and serve it from plain Spring MVC — no extra wiring:
public class Report {
public String title;
public Attachment document; // dev.restxop.Attachment
}
@GetMapping(value = "/report", produces = "multipart/related")
Report report() {
return new Report("Q3 report",
Attachment.builder(Path.of("/data/q3.pdf"))
.contentType("application/pdf")
.build());
}On the receiving side, report.document.contentStream() hands you the bytes as they arrive — never fully buffered.
npm install restxop-jsimport { restxopFetch } from "restxop-js";
// The typed JSON payload resolves as soon as the root part lands…
const { payload, attachments } = await restxopFetch<Report>("/report");
console.log(payload.title); // usable immediately
// …while the attachment streams in behind it.
const pdf = await attachments[0].blob();Zero runtime dependencies, ESM-only, under 10 KB gzipped; runs in evergreen browsers and Node 20+.
RestXOP is early and actively developed. Feedback, issues, and contributions are welcome — if you've ever winced at Base64-ing a large file into a JSON body, this project is for you.
Licensed under the Apache License 2.0.