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

Form Parser

This example shows how to parse application/x-www-form-urlencoded request bodies with Vix middleware.

Use this when a route receives classic HTML form data such as:

txt
name=Gaspard&email=gaspard@example.com&message=Hello

This format is commonly used by:

txt
HTML forms
simple contact forms
login forms
search forms
small admin forms
browser-submitted forms

For file uploads, use multipart instead.

For JSON APIs, use the JSON parser.

What this example builds

The app exposes:

txt
GET  /
POST /contact

The route /contact expects:

txt
Content-Type: application/x-www-form-urlencoded

and fields:

txt
name
email
message

The middleware parses the form and stores it in request state as:

cpp
middleware::parsers::FormBody

Project structure

Create:

txt
form_parser_demo/
└── form_parser.cpp

Create the file:

bash
mkdir form_parser_demo
cd form_parser_demo
touch form_parser.cpp

Source

Open:

txt
form_parser.cpp

Add:

cpp
#include <string>

#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

static std::string form_value(
  const middleware::parsers::FormBody &form,
  const std::string &key)
{
  auto it = form.fields.find(key);

  if (it == form.fields.end())
    return {};

  return it->second;
}

static bool missing(const std::string &value)
{
  return value.empty();
}

static void install_middleware(App &app)
{
  app.use("/contact", middleware::app::request_id_dev());
  app.use("/contact", middleware::app::timing_dev());
  app.use("/contact", middleware::app::security_headers_dev());

  app.use("/contact", middleware::app::body_limit_write_dev(
    16 * 1024
  ));

  app.use("/contact", middleware::app::form_dev(
    4096
  ));
}

static void register_routes(App &app)
{
  app.get("/", [](Request &, Response &res)
  {
    res.send(
      "Form parser example\n"
      "\n"
      "Try:\n"
      "  curl -i -X POST http://127.0.0.1:8080/contact \\\n"
      "    -H \"Content-Type: application/x-www-form-urlencoded\" \\\n"
      "    --data \"name=Gaspard&email=gaspard@example.com&message=Hello\"\n"
    );
  });

  app.post("/contact", [](Request &req, Response &res)
  {
    auto &form =
      req.state<middleware::parsers::FormBody>();

    const std::string name = form_value(form, "name");
    const std::string email = form_value(form, "email");
    const std::string message = form_value(form, "message");

    if (missing(name))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "name"
      });
      return;
    }

    if (missing(email))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "email"
      });
      return;
    }

    if (missing(message))
    {
      res.status(422).json({
        "ok", false,
        "error", "Missing required field",
        "field", "message"
      });
      return;
    }

    res.json({
      "ok", true,
      "received", true,
      "name", name,
      "email", email,
      "message", message
    });
  });
}

int main()
{
  App app;

  install_middleware(app);
  register_routes(app);

  app.run(8080);
  return 0;
}

Run it

Run:

bash
vix run form_parser.cpp

The server listens on:

txt
http://127.0.0.1:8080

Test the home route

bash
curl -i http://127.0.0.1:8080/

Expected body:

txt
Form parser example

This route is public and does not use the form parser.

Send a valid form

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com&message=Hello"

Expected status:

txt
200 OK

Expected body:

json
{
  "ok": true,
  "received": true,
  "name": "Gaspard",
  "email": "gaspard@example.com",
  "message": "Hello"
}

The form parser decodes the body and stores the fields in request state.

URL decoding

The parser handles URL-encoded values.

Example:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard+Kirira&email=gaspard%40example.com&message=Hello+from+Vix"

Expected decoded values:

txt
name
  Gaspard Kirira

email
  gaspard@example.com

message
  Hello from Vix

In URL-encoded forms:

txt
+
  becomes a space

%40
  becomes @

Test missing field

Missing message:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com"

Expected status:

txt
422 Unprocessable Entity

Expected body:

json
{
  "ok": false,
  "error": "Missing required field",
  "field": "message"
}

The middleware parses the HTTP body.

The handler validates application fields.

Test wrong Content-Type

Send JSON to the form route:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/json" \
  --data '{"name":"Gaspard"}'

Expected status:

txt
415 Unsupported Media Type

The form parser expects:

txt
application/x-www-form-urlencoded

If a route expects JSON, use the JSON parser instead.

Test payload too large

The example installs a broad body limit:

cpp
app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

and a form parser limit:

cpp
app.use("/contact", middleware::app::form_dev(
  4096
));

To test the form parser limit:

bash
BIG="$(python3 -c 'print("name=Gaspard&email=gaspard@example.com&message=" + "x"*5000)')"

curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "$BIG"

Expected status:

txt
413 Payload Too Large

Use body limits and parser limits together:

txt
body limit
  protects the route from large requests

parser limit
  protects the parser from large form bodies

How it works

The key middleware is:

cpp
app.use("/contact", middleware::app::form_dev(
  4096
));

It checks:

txt
Content-Type
body size
URL-encoded format

Then it stores:

cpp
middleware::parsers::FormBody

The route reads it with:

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

The fields are available in:

cpp
form.fields

which is a map of:

txt
string key
string value

Middleware order

The example installs middleware in this order:

cpp
app.use("/contact", middleware::app::request_id_dev());
app.use("/contact", middleware::app::timing_dev());
app.use("/contact", middleware::app::security_headers_dev());

app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));

The order matters:

txt
request id
  identify the request

timing
  measure the request

security headers
  harden the response

body limit
  reject oversized bodies early

form parser
  parse application/x-www-form-urlencoded

handler
  validates fields and returns response

The body limit should run before the parser.

Why the parser is route-specific

The form parser is installed only on:

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

not on:

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

That matters because most routes do not receive URL-encoded forms.

Good:

cpp
app.use("/contact", middleware::app::form_dev(4096));

Risky:

cpp
app.use("/", middleware::app::form_dev(4096));

Install parsers only where the route expects that body format.

HTML form example

You can send the same request from a browser form.

html
<form method="post" action="/contact">
  <label>
    Name
    <input name="name" />
  </label>

  <label>
    Email
    <input name="email" type="email" />
  </label>

  <label>
    Message
    <textarea name="message"></textarea>
  </label>

  <button type="submit">Send</button>
</form>

By default, a normal HTML form like this sends:

txt
application/x-www-form-urlencoded

If the form includes file inputs, use:

html
enctype="multipart/form-data"

and use the multipart middleware instead.

Form parser vs JSON parser

Use form parser for classic HTML forms:

txt
application/x-www-form-urlencoded

Use JSON parser for API clients:

txt
application/json

Use multipart parser for files:

txt
multipart/form-data

Simple rule:

txt
HTML form without files
  form parser

API request with JSON body
  JSON parser

HTML form with files
  multipart parser

Add CORS for frontend apps

If a browser frontend runs on another origin, add CORS before the parser:

cpp
app.use("/contact", middleware::app::cors_dev({
  "http://localhost:5173",
  "http://127.0.0.1:5173"
}));

app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));

CORS should run before body parsers so preflight requests can be handled cleanly.

Production notes

For production forms, add:

txt
CSRF protection for browser forms
rate limiting
server-side validation
email format validation
spam protection
logging
request id
clear error responses

For forms submitted by browsers, CSRF protection is important.

Example middleware shape:

cpp
app.use("/contact", middleware::app::csrf_dev());
app.use("/contact", middleware::app::form_dev(4096));

The exact setup depends on how your frontend obtains and sends the CSRF token.

Complete test flow

Run:

bash
vix run form_parser.cpp

Valid form:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com&message=Hello"

URL-decoded form:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard+Kirira&email=gaspard%40example.com&message=Hello+from+Vix"

Missing field:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "name=Gaspard&email=gaspard@example.com"

Wrong content type:

bash
curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/json" \
  --data '{"name":"Gaspard"}'

Large body:

bash
BIG="$(python3 -c 'print("name=Gaspard&email=gaspard@example.com&message=" + "x"*5000)')"

curl -i \
  -X POST http://127.0.0.1:8080/contact \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "$BIG"

Summary

Use the form parser when a route receives:

txt
application/x-www-form-urlencoded

The core setup is:

cpp
app.use("/contact", middleware::app::body_limit_write_dev(
  16 * 1024
));

app.use("/contact", middleware::app::form_dev(
  4096
));

Then read the parsed form:

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

Access fields:

cpp
auto name = form.fields["name"];

The mental model is:

txt
body_limit
  protects the route

form_dev
  parses URL-encoded fields

handler
  validates application data

Released under the MIT License.