Vix.cpp v2.6.0 is here Read the blog
Skip to content

Multipart Upload

This example shows how to handle multipart/form-data uploads with Vix middleware.

Use this when you need routes that receive:

txt
files
images
documents
form fields
mixed form data

The example uses:

cpp
#include <vix/middleware.hpp>

and the App preset:

cpp
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:

txt
GET  /
POST /upload

Uploaded files are saved into:

txt
uploads/

The response returns JSON describing what was received.

Project structure

Create:

txt
multipart_upload_demo/
├── multipart_upload.cpp
└── uploads/

Create the directory:

bash
mkdir -p multipart_upload_demo/uploads
cd multipart_upload_demo
touch multipart_upload.cpp

Source

Open:

txt
multipart_upload.cpp

Add:

cpp
#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:

bash
vix run multipart_upload.cpp

The server listens on:

txt
http://127.0.0.1:8080

Create a test file

Create a small file to upload:

bash
echo "Hello from Vix multipart upload" > sample.txt

Upload one file

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -F "title=My first upload" \
  -F "file=@sample.txt"

Expected status:

txt
200 OK

Expected body shape:

json
{
  "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:

txt
the form field is parsed
the file is saved
the parsed multipart state is available in the handler

Upload multiple fields

bash
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:

bash
echo "Second file" > sample2.txt

Send both files:

bash
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:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -H "Content-Type: text/plain" \
  --data "hello"

Expected status:

txt
415 Unsupported Media Type

The multipart middleware expects:

txt
Content-Type: multipart/form-data

Test missing boundary

Force a multipart content type without a boundary:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -H "Content-Type: multipart/form-data" \
  --data "x"

Expected status:

txt
400 Bad Request

A valid multipart request needs a boundary.

When you use:

bash
-F "file=@sample.txt"

curl automatically creates the correct boundary.

Test body limit

The example installs:

cpp
app.use("/upload", middleware::app::body_limit_write_dev(
  10 * 1024 * 1024
));

This limits upload request bodies to roughly:

txt
10 MiB

To test a smaller limit, temporarily change it to:

cpp
app.use("/upload", middleware::app::body_limit_write_dev(
  1024
));

Then upload a larger file.

Expected status:

txt
413 Payload Too Large

Use body limits before multipart parsing so oversized requests are rejected early.

How it works

The important middleware is:

cpp
app.use("/upload", middleware::app::multipart_save_dev(
  "uploads"
));

It does three things:

txt
validates multipart/form-data
saves uploaded files into uploads/
stores MultipartForm in request state

The route reads the parsed form from request state:

cpp
auto &form =
  req.state<middleware::parsers::MultipartForm>();

Then it returns a JSON representation:

cpp
res.json(middleware::app::multipart_json(form));

Middleware order

The example installs middleware in this order:

cpp
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:

txt
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 MultipartForm

The body limit should run before the multipart parser.

Why the parser is route-specific

The multipart parser is installed only on:

cpp
app.use("/upload", ...)

not globally on:

cpp
app.use("/", ...)

That matters because most routes do not receive multipart bodies.

Good:

cpp
app.use("/upload", middleware::app::multipart_save_dev("uploads"));

Risky:

cpp
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:

txt
application/json

Use multipart for uploads:

txt
multipart/form-data

A product creation route with images may use multipart because it sends both fields and files:

txt
title
price
description
image files

A pure API route without files should usually use JSON.

Add simple field validation

You can validate fields after reading MultipartForm.

Example shape:

cpp
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:

txt
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 paths

Do 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:

txt
uploads/

For development, that is fine.

For production, prefer a configured path:

dotenv
UPLOADS_PATH=storage/uploads

Then wire it from config:

cpp
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:

txt
uploads/
storage/uploads/

Complete test flow

Run:

bash
vix run multipart_upload.cpp

Create test files:

bash
echo "Hello from Vix" > sample.txt
echo "Second file" > sample2.txt

Upload one file:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -F "title=One file" \
  -F "file=@sample.txt"

Upload two files:

bash
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:

bash
curl -i \
  -X POST http://127.0.0.1:8080/upload \
  -H "Content-Type: text/plain" \
  --data "hello"

Inspect uploaded files:

bash
ls -la uploads/

Summary

Use multipart upload when a route receives files.

The core setup is:

cpp
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:

cpp
auto &form =
  req.state<middleware::parsers::MultipartForm>();

And return or process it:

cpp
res.json(middleware::app::multipart_json(form));

The mental model is:

txt
body_limit
  protects the server

multipart_save_dev
  parses fields and saves files

handler
  validates application data and returns a response

Released under the MIT License.