HTML-формы
API принимает JSON, а обычная HTML-страница отправляет данные формой: браузер собирает поля <input> и <textarea> и шлёт их запросом POST в формате title=...&body=... (тип application/x-www-form-urlencoded). Добавим в блог страницу «Новый пост»: форму, её обработку, показ ошибок и перенаправление на список постов.
Шаблон формы
Создайте шаблон рядом с posts.mustache:
<!-- src/main/resources/templates/new-post.mustache -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Новый пост</title>
</head>
<body>
<h1>Новый пост</h1>
{{#errors}}
<p style="color: red">{{.}}</p>
{{/errors}}
<form method="post" action="/blog">
<p><label>Заголовок <input name="title" value="{{post.title}}"></label></p>
<p><label>Текст <textarea name="body">{{post.body}}</textarea></label></p>
<p><label>Автор <input name="author" value="{{post.author}}"></label></p>
<button type="submit">Опубликовать</button>
</form>
</body>
</html>Атрибут name у поля — это имя, под которым значение придёт на сервер. {{.}} выводит текущий элемент списка errors, а {{post.title}} подставляет введённое значение, чтобы после ошибки не набирать всё заново. В posts.mustache под заголовком добавьте ссылку <p><a href="/blog/new">Написать пост</a></p>.
Обработчики
Добавьте в PostController два метода: показать форму и принять её.
// 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("Нужен заголовок");
}
if (data.body().length() < 5) {
errors.add("Текст слишком короткий");
}
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") возвращает значение поля или null, если его нет. Если есть ошибки, тот же шаблон рисуется снова с кодом 400, списком ошибок и введёнными данными. Импорты: java.util.ArrayList и java.util.List.
Маршруты добавьте в блок apiBuilder:
// src/main/java/com/example/blog/Main.java
get("/blog/new", posts::newForm);
post("/blog", posts::submit);Post/Redirect/Get
После успешной отправки сервер не рисует страницу, а отвечает перенаправлением (redirect): кодом 303 See Other и заголовком Location: /blog. Браузер сам открывает /blog запросом GET. Этот приём называют Post/Redirect/Get: если пользователь обновит страницу, браузер повторит безопасный GET, а не отправит форму второй раз и не создаст дубликат.
Проверка
Откройте http://localhost:8080/blog/new и отправьте пустую форму — над ней появятся ошибки. То же через curl: ключ -d отправляет поля как форму, а --data-urlencode кодирует русские буквы.
curl -i http://localhost:8080/blog -d "title=" -d "body=abc" --data-urlencode "author=Олег"HTTP/1.1 400 Bad Request
Content-Type: text/html
...
<h1>Новый пост</h1>
<p style="color: red">Нужен заголовок</p>
<p style="color: red">Текст слишком короткий</p>
<form method="post" action="/blog">
<p><label>Заголовок <input name="title" value=""></label></p>
<p><label>Текст <textarea name="body">abc</textarea></label></p>
<p><label>Автор <input name="author" value="Олег"></label></p>
...
Теперь правильные данные:
curl -i http://localhost:8080/blog --data-urlencode "title=Формы" \
--data-urlencode "body=Форма отправляет поля." --data-urlencode "author=Анна"HTTP/1.1 303 See Other
Content-Type: text/plain
Location: /blog
Redirected
Откройте http://localhost:8080/blog — новый пост «Формы» стоит последним в списке.
Частая ошибка
Данные формы — не JSON. Если отправить ту же форму на API-маршрут POST /posts, где стоит bodyValidator, Jackson не сможет прочитать тело:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"REQUEST_BODY":[{"message":"DESERIALIZATION_FAILED","args":{},"value":"title=%D0%A4%D0%BE%D1%80%D0%BC%D1%8B&body=..."}]}
Для форм читайте поля через ctx.formParam, а bodyAsClass и bodyValidator оставьте для JSON.