HTML forms
The API accepts JSON, but a regular HTML page sends data with a form: the browser collects the <input> and <textarea> fields and sends them in a POST request in the title=...&body=... format (the application/x-www-form-urlencoded type). Let us add a "New post" page to the blog: the form, its handling, showing errors and redirecting to the list of posts.
The form template
Create a template next to posts.mustache:
<!-- src/main/resources/templates/new-post.mustache -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>New post</title>
</head>
<body>
<h1>New post</h1>
{{#errors}}
<p style="color: red">{{.}}</p>
{{/errors}}
<form method="post" action="/blog">
<p><label>Title <input name="title" value="{{post.title}}"></label></p>
<p><label>Text <textarea name="body">{{post.body}}</textarea></label></p>
<p><label>Author <input name="author" value="{{post.author}}"></label></p>
<button type="submit">Publish</button>
</form>
</body>
</html>The name attribute of a field is the name under which the value reaches the server. {{.}} prints the current item of the errors list, and {{post.title}} puts back the entered value so the user does not have to type everything again after an error. In posts.mustache, add a link under the heading: <p><a href="/blog/new">Write a post</a></p>.
The handlers
Add two methods to PostController: one shows the form, the other accepts it.
// src/main/java/com/example/blog/PostController.java
public void newForm(Context ctx) {
ctx.render("templates/new-post.mustache");
}
public void submit(Context ctx) {
var data = new NewPost(field(ctx, "title"), field(ctx, "body"), field(ctx, "author"));
List<String> errors = new ArrayList<>();
if (data.title().isBlank()) {
errors.add("Title is required");
}
if (data.body().length() < 5) {
errors.add("Body is too short");
}
if (!errors.isEmpty()) {
ctx.status(HttpStatus.BAD_REQUEST);
ctx.render("templates/new-post.mustache", Map.of("errors", errors, "post", data));
return;
}
store.create(data);
ctx.redirect("/blog", HttpStatus.SEE_OTHER);
}
private static String field(Context ctx, String name) {
String value = ctx.formParam(name);
return value == null ? "" : value.strip();
}ctx.formParam("title") returns the field value, or null if there is no such field. When there are errors, the same template is rendered again with status 400, the list of errors and the entered data. Imports: java.util.ArrayList and java.util.List.
Add the routes to the apiBuilder block:
// src/main/java/com/example/blog/Main.java
get("/blog/new", posts::newForm);
post("/blog", posts::submit);Post/Redirect/Get
After a successful submit the server does not render a page; it answers with a redirect: status 303 See Other and the header Location: /blog. The browser then opens /blog with a GET request on its own. This technique is called Post/Redirect/Get: if the user reloads the page, the browser repeats a safe GET instead of sending the form a second time and creating a duplicate.
Trying it out
Open http://localhost:8080/blog/new and submit an empty form — errors appear above it. The same with curl: the -d option sends fields as a form.
curl -i http://localhost:8080/blog -d "title=" -d "body=abc" -d "author=Oleg"HTTP/1.1 400 Bad Request
Content-Type: text/html
...
<h1>New post</h1>
<p style="color: red">Title is required</p>
<p style="color: red">Body is too short</p>
<form method="post" action="/blog">
<p><label>Title <input name="title" value=""></label></p>
<p><label>Text <textarea name="body">abc</textarea></label></p>
<p><label>Author <input name="author" value="Oleg"></label></p>
...
Now valid data. The --data-urlencode option encodes spaces and other special characters:
curl -i http://localhost:8080/blog --data-urlencode "title=Forms" \
--data-urlencode "body=A form sends fields." --data-urlencode "author=Anna"HTTP/1.1 303 See Other
Content-Type: text/plain
Location: /blog
Redirected
Open http://localhost:8080/blog — the new post "Forms" is the last one in the list.
Common mistake
Form data is not JSON. If you send the same form to the API route POST /posts, which uses bodyValidator, Jackson cannot read the body:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"REQUEST_BODY":[{"message":"DESERIALIZATION_FAILED","args":{},"value":"title=Forms&body=A form sends fields.&author=Anna"}]}
For forms, read the fields with ctx.formParam, and keep bodyAsClass and bodyValidator for JSON.