Multipart Upload
This example shows how to handle multipart/form-data uploads with Vix middleware.
Use this when you need routes that receive:
files
images
documents
form fields
mixed form dataThe example uses:
#include <vix/middleware.hpp>and the App preset:
middleware::app::multipart_save_dev("uploads")The middleware validates the multipart request, parses the form, saves uploaded files, and stores the parsed result in request state.
What this example builds
The app exposes:
GET /
POST /uploadUploaded files are saved into:
uploads/The response returns JSON describing what was received.
Project structure
Create:
multipart_upload_demo/
├── multipart_upload.cpp
└── uploads/Create the directory:
mkdir -p multipart_upload_demo/uploads
cd multipart_upload_demo
touch multipart_upload.cppSource
Open:
multipart_upload.cppAdd:
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
static void install_middleware(App &app)
{
app.use("/upload", middleware::app::request_id_dev());
app.use("/upload", middleware::app::timing_dev());
app.use("/upload", middleware::app::security_headers_dev());
app.use("/upload", middleware::app::body_limit_write_dev(
10 * 1024 * 1024
));
app.use("/upload", middleware::app::multipart_save_dev(
"uploads"
));
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.send(
"Multipart upload example\n"
"\n"
"Try:\n"
" curl -i -X POST http://127.0.0.1:8080/upload \\\n"
" -F \"title=My file\" \\\n"
" -F \"file=@README.md\"\n"
);
});
app.post("/upload", [](Request &req, Response &res)
{
auto &form =
req.state<middleware::parsers::MultipartForm>();
res.json(middleware::app::multipart_json(form));
});
}
int main()
{
App app;
install_middleware(app);
register_routes(app);
app.run(8080);
return 0;
}Run it
Run:
vix run multipart_upload.cppThe server listens on:
http://127.0.0.1:8080Create a test file
Create a small file to upload:
echo "Hello from Vix multipart upload" > sample.txtUpload one file
curl -i \
-X POST http://127.0.0.1:8080/upload \
-F "title=My first upload" \
-F "file=@sample.txt"Expected status:
200 OKExpected body shape:
{
"fields": {
"title": "My first upload"
},
"files": [
{
"field": "file",
"filename": "sample.txt",
"path": "uploads/...",
"size": 32
}
]
}The exact file path and JSON shape can depend on the current multipart serialization helper, but the important result is:
the form field is parsed
the file is saved
the parsed multipart state is available in the handlerUpload multiple fields
curl -i \
-X POST http://127.0.0.1:8080/upload \
-F "title=Product image" \
-F "category=electronics" \
-F "description=Main product photo" \
-F "file=@sample.txt"The middleware parses normal form fields and file fields from the same request.
Upload multiple files
Create another test file:
echo "Second file" > sample2.txtSend both files:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-F "title=Multiple files" \
-F "files=@sample.txt" \
-F "files=@sample2.txt"This is useful for product images, galleries, documents, and attachments.
Test invalid content type
Send plain text instead of multipart:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-H "Content-Type: text/plain" \
--data "hello"Expected status:
415 Unsupported Media TypeThe multipart middleware expects:
Content-Type: multipart/form-dataTest missing boundary
Force a multipart content type without a boundary:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-H "Content-Type: multipart/form-data" \
--data "x"Expected status:
400 Bad RequestA valid multipart request needs a boundary.
When you use:
-F "file=@sample.txt"curl automatically creates the correct boundary.
Test body limit
The example installs:
app.use("/upload", middleware::app::body_limit_write_dev(
10 * 1024 * 1024
));This limits upload request bodies to roughly:
10 MiBTo test a smaller limit, temporarily change it to:
app.use("/upload", middleware::app::body_limit_write_dev(
1024
));Then upload a larger file.
Expected status:
413 Payload Too LargeUse body limits before multipart parsing so oversized requests are rejected early.
How it works
The important middleware is:
app.use("/upload", middleware::app::multipart_save_dev(
"uploads"
));It does three things:
validates multipart/form-data
saves uploaded files into uploads/
stores MultipartForm in request stateThe route reads the parsed form from request state:
auto &form =
req.state<middleware::parsers::MultipartForm>();Then it returns a JSON representation:
res.json(middleware::app::multipart_json(form));Middleware order
The example installs middleware in this order:
app.use("/upload", middleware::app::request_id_dev());
app.use("/upload", middleware::app::timing_dev());
app.use("/upload", middleware::app::security_headers_dev());
app.use("/upload", middleware::app::body_limit_write_dev(
10 * 1024 * 1024
));
app.use("/upload", middleware::app::multipart_save_dev(
"uploads"
));The order matters:
request id
identify the request
timing
measure the request
security headers
harden the response
body limit
reject oversized uploads early
multipart parser
parse fields and save files
handler
uses MultipartFormThe body limit should run before the multipart parser.
Why the parser is route-specific
The multipart parser is installed only on:
app.use("/upload", ...)not globally on:
app.use("/", ...)That matters because most routes do not receive multipart bodies.
Good:
app.use("/upload", middleware::app::multipart_save_dev("uploads"));Risky:
app.use("/", middleware::app::multipart_save_dev("uploads"));A parser should run only where that body format is expected.
Multipart vs JSON
Use JSON for structured API payloads:
application/jsonUse multipart for uploads:
multipart/form-dataA product creation route with images may use multipart because it sends both fields and files:
title
price
description
image filesA pure API route without files should usually use JSON.
Add simple field validation
You can validate fields after reading MultipartForm.
Example shape:
app.post("/upload", [](Request &req, Response &res)
{
auto &form =
req.state<middleware::parsers::MultipartForm>();
auto title = form.fields.find("title");
if (title == form.fields.end() || title->second.empty())
{
res.status(422).json({
"ok", false,
"error", "Missing required field",
"field", "title"
});
return;
}
res.json(middleware::app::multipart_json(form));
});Use this pattern for required text fields.
Production notes
For production uploads, add application-level checks.
At minimum:
limit total body size
limit number of files
limit individual file size
validate file extension
validate MIME type
rename files safely
avoid trusting original filenames
store outside public directory when needed
scan files if required by your app
return stable file IDs instead of raw local pathsDo not trust client-provided filenames.
A client can send names with unsafe characters or misleading extensions.
The application should decide the final storage name.
Upload directory
This example saves to:
uploads/For development, that is fine.
For production, prefer a configured path:
UPLOADS_PATH=storage/uploadsThen wire it from config:
const std::string uploads_path =
cfg.getString("uploads.path", "storage/uploads");
app.use("/upload", middleware::app::multipart_save_dev(
uploads_path
));Keep uploaded files out of source-controlled directories.
A typical .gitignore should include:
uploads/
storage/uploads/Complete test flow
Run:
vix run multipart_upload.cppCreate test files:
echo "Hello from Vix" > sample.txt
echo "Second file" > sample2.txtUpload one file:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-F "title=One file" \
-F "file=@sample.txt"Upload two files:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-F "title=Two files" \
-F "files=@sample.txt" \
-F "files=@sample2.txt"Invalid content type:
curl -i \
-X POST http://127.0.0.1:8080/upload \
-H "Content-Type: text/plain" \
--data "hello"Inspect uploaded files:
ls -la uploads/Summary
Use multipart upload when a route receives files.
The core setup is:
app.use("/upload", middleware::app::body_limit_write_dev(
10 * 1024 * 1024
));
app.use("/upload", middleware::app::multipart_save_dev(
"uploads"
));Then read the parsed form:
auto &form =
req.state<middleware::parsers::MultipartForm>();And return or process it:
res.json(middleware::app::multipart_json(form));The mental model is:
body_limit
protects the server
multipart_save_dev
parses fields and saves files
handler
validates application data and returns a response