投稿日
サービス開発リファレンスを使ってバリデーションしてみよう
はじめに
こんにちは。西日本テクノロジー&イノベーション室の齊藤です。
私の所属する西日本テクノロジー&イノベーション室では、2020年9月末に「SPA + REST API構成のサービス開発リファレンス(以下サービス開発リファレンス)」を公開しました。こちらは、シングルページアプリケーション(以下SPA)とREST APIから構成されるWebアプリケーションを開発する際に活用して頂けるコンテンツになっています。
前回のブログでは、Todo一覧取得のREST APIを作り、タスク管理をするための簡易的な画面(Todo画面)とつなぎこむ実装をご紹介しました。
今回はタスク登録処理の実装を行いながら、バリデーションのやり方を解説します。
- サービス開発リファレンスを使ってWebアプリケーションを作成してみよう<導入編>
- サービス開発リファレンスを使って1画面作成してみよう
- サービス開発リファレンスを使ってREST APIを1つ作成してみよう
- サービス開発リファレンスを使ってバリデーションしてみよう(←今回の記事)
事前準備
1~3までの記事でご紹介した作業の完成状態をベースに作成しますので、こちらの作業がまだの方は実施してみてください。
バリデーションの方針
フロントエンドのUIで入力された値のバリデーションは、フロントエンド・バックエンドの両方で行います。
バリデーションをフロントエンドで行う理由は、バリデーションのためにHTTP通信する必要がなく、バリデーション完了後の正しい状態となっている値だけを送信できるためです。 また、バリデーションがHTTPリクエストを伴わないためバリデーションの結果を速やかにユーザーへ通知できます。
バックエンドでもバリデーションを行う理由は、不正なリクエストからアプリケーションを守るためです。 前述のようにフロントエンドでバリデーションを行うため、フロントエンドのUIで入力された値であれば必ず正しい状態でバックエンドへ送信されます。 しかし、ブラウザの開発者コンソールで不正な値を送信するJavaScriptプログラムを実行したり、curlやその他のHTTPクライアントを用いて不正な値を直接バックエンドへ送信できま。 そういった不正なリクエストを阻止するために、バックエンドでも必ずバリデーションは行います。
この場合、正しくフロントエンドのUIを使用しているならバックエンドではバリデーションエラーに成り得ません。そのため、バックエンドのバリデーションではユーザーフレンドリーなエラーメッセージを返す必要はありません。
ただし、DBの値で存在チェックを行うといった、フロントエンドからは参照できないリソースが必要となるバリデーションがあります。
この場合はフロントエンドではバリデーションができないので、バックエンドでのバリデーションのみを行います。
バックエンド
まずはバリデーションの機能を実装するために、Todoを登録するREST APIを作成します。
Todo登録機能の実装
最新のTodoIdを取得するためのSQLを作成します。
backend/src/main/resources/com/example/infrastructure/persistence/entity/TodoEntity.sql
FIND_MAX_TODO_ID =
SELECT
MAX(TODO_ID) AS TODO_ID
FROM
TODO
次に、アクションクラスにTodoを登録するメソッドを以下のように追加します。
backend/src/main/java/com/example/presentation/restapi/todo/TodosAction.java
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public TodoResponse post(PostRequest requestBody) {
TodoEntity entity = UniversalDao.findBySqlFile(TodoEntity.class, "FIND_MAX_TODO_ID", new Object[0]);
Long newTodoId = entity.getTodoId() + 1;
TodoEntity todoEntity = new TodoEntity();
todoEntity.setTodoId(newTodoId);
todoEntity.setText(requestBody.text);
UniversalDao.insert(todoEntity);
return new TodoResponse(newTodoId, requestBody.text);
}
public static class PostRequest {
public String text;
}
バリデーションの実装
次にバリデーションを実装します。
入力値をチェックするため、オブジェクトを受け取った後はValidatorUtilを使用してBeanValidationを実行します。
フロントエンドから受け取るtext
は、空文字を許容しないようにするため javax.validation.constraints.NotBlank
アノテーションを付与します。
ここまで実装すると、TodosAction
は以下のようになります。
backend/src/main/java/com/example/presentation/restapi/todo/TodosAction.java
package com.example.presentation.restapi.todo;
import com.example.infrastructure.persistence.entity.TodoEntity;
import nablarch.common.dao.EntityList;
import nablarch.common.dao.UniversalDao;
import nablarch.core.repository.di.config.externalize.annotation.SystemRepositoryComponent;
import nablarch.core.validation.ee.ValidatorUtil;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.stream.Collectors;
@SystemRepositoryComponent
@Path("/todos")
public class TodosAction {
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<TodoResponse> get() {
EntityList<TodoEntity> todoEntities = UniversalDao.findAll(TodoEntity.class);
return todoEntities.stream()
.map(entity -> new TodoResponse(entity.getTodoId(), entity.getText()))
.collect(Collectors.toList());
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public TodoResponse post(PostRequest requestBody) {
ValidatorUtil.validate(requestBody);
TodoEntity entity = UniversalDao.findBySqlFile(TodoEntity.class, "FIND_MAX_TODO_ID", new Object[0]);
Long newTodoId = entity.getTodoId() + 1;
TodoEntity todoEntity = new TodoEntity();
todoEntity.setTodoId(newTodoId);
todoEntity.setText(requestBody.text);
UniversalDao.insert(todoEntity);
return new TodoResponse(newTodoId, requestBody.text);
}
public static class TodoResponse {
public final Long id;
public final String text;
public TodoResponse(Long id, String text) {
this.id = id;
this.text = text;
}
}
public static class PostRequest {
@NotBlank
public String text;
}
}
サーバの起動
ここまで実装できたら、サーバを起動してみましょう。
以下コマンドでまずデータベースを起動します。
$ docker-compose up -d
次のようにdocker-compose ps
コマンドを実行してState
がUp
になっていれば起動成功です。
$ docker-compose ps
Name Command State Ports
-----------------------------------------------------------------------------------------------------------------
example_postgres_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp
example_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:6379->6379/tcp
次に、backend
ディレクトリで以下コマンドを実行してサーバを起動しましょう。
$ mvn jetty:run
動作確認はフロントエンドとあわせて実施します。
フロントエンド
Todoを登録するREST APIが作成できましたので、フロントエンドから呼び出すようにします。
Todo登録機能のつなぎこみ
Todo登録のREST APIを呼び出すため、BackendService
に以下を追加します。
frontend/src/example/backend/BackendService.ts
const postTodo = async (text: string) => {
Logger.debug('call service of postTodo');
const response = await restClient.post('/api/todos', {text});
Logger.debug(response);
if (response.ok) {
return;
}
throw new Error(`Web API call failed. [ status code: ${response.status} ]`);
};
そして、export default
にpostTodo
を追加します。
export default {
getHelloWorld,
getTodos,
postTodo,
};
次に、Todo.tsx
の handleSubmit
を以下のように修正してTodoを登録できるようにします。
frontend/src/example/components/pages/Todo.tsx
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
BackendService.postTodo(text)
.then(() => setTodos([...todos, text]))
};
これでSPA側の実装も完了したため、以下コマンドをfrontend
ディレクトリで実行してみましょう。
$ npm start
自動でブラウザが立ち上がり、Top画面が表示されているはずです。
以下URLのTodo画面を開いて、実際にTodoを登録してみましょう。
http://localhost:3000/todo
このように新規追加
というタスクが追加されました。
バリデーションの実装
では、ここからフロントエンドにバリデーションを実装します。
まずは、すでに定義されているhandleSubmit
をpostTodo
という名前に変更します。
const postTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
BackendService.postTodo(text)
.then(() => setTodos([...todos, text]));
};
次に、バリデーション用のフックであるuseValidationを使用してバリデーションを実装します。 このuseValidation
はサービス開発リファレンスで用意されているもので、必要に応じて再利用いただけます。
const { error, handleSubmit } = useValidation<{text: string}>({
text: stringField()
.required('やることは必須です。')
.maxLength(20, 'やることは20字以内で入力してください。'),
});
useValidation
では、型引数により各項目で使用できるバリデーションを制限します。そのため、まずはバリデーション対象になる項目を定義します。 useValidation
の型引数に{text: string}
を指定し、引数にバリデーションを定義したオブジェクトを生成して渡します。プロパティのキーは型引数に対応し、各項目でどのようなバリデーションを行うかを定義します。
string型の項目を検証するためのstringFields
が用意されているので、これを起点にバリデーションを指定していきます。stringFields
では、必須入力をチェックするためのrequired
や、最大文字数をチェックするためのmaxLength
等が用意されており、ここでのバリデーションはそれらを使用します。
useValidation
の戻り値として、バリデーションエラーが格納されるerror
と、サブミット時に自動で実行するためのhandleSubmit
を受け取ります。
error
のプロパティはuseValidation
のプロパティに一対一で対応しています。例えば、text
に対するバリデーションでエラーとなった場合、error.text
にエラーメッセージが設定されます。
<input type="text" {...textAttributes} placeholder="やることを入力してください" />
<ValidationError message={error.text}/>
次に、各テキストボックスの下にエラーメッセージを表示するように、input
の直後にエラーメッセージを表示するようにします。 ValidationError
というエラーを表示するためのコンポーネントが用意されていますので、そちらを使用します。
<form className="form" onSubmit={handleSubmit({ text }, postTodo)}>
handleSubmit
は関数であり、第1引数に入力値と、第2引数にエラーが発生しなかった場合のコールバック関数を渡します。 こちらをform
のonSubmit
に設定します。
ここまで実装すると、以下のようになります。
frontend/src/example/components/pages/Todo.tsx
import React, {useEffect, useState} from 'react';
import {Logger, stringField, useInput, useValidation} from '../../../framework';
import { BackendService } from '../../backend';
import {ValidationError} from '../basics';
import './Todo.css';
type Todo = {
id: number
text: string
}
const Todo: React.FC = () => {
const [todos, setTodos] = useState<string[]>([]);
const [text, textAttributes] = useInput('');
const { error, handleSubmit } = useValidation<{text: string}>({
text: stringField()
.required('やることは必須です。')
.maxLength(20, 'やることは20字以内で入力してください。'),
});
useEffect(() => {
BackendService.getTodos()
.then(response => {
Logger.debug(response);
setTodos(response.map((todo: Todo) => todo.text));
});
}, []);
const postTodo = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
BackendService.postTodo(text)
.then(() => setTodos([...todos, text]));
};
return (
<div className="content">
<div className="form_field">
<form className="form" onSubmit={handleSubmit({ text }, postTodo)}>
<div className="input_field">
<input type="text" {...textAttributes} placeholder="やることを入力してください" />
<ValidationError message={error.text}/>
</div>
<div className="button_field">
<button type="submit">追加</button>
</div>
</form>
</div>
<ul className="list">
{todos.map((todo, index) =>
<li className="item" key={index}>
<div className="todo">
<span>{todo}</span>
</div>
</li>
)}
</ul>
</div>
);
};
export default Todo;
これでバリデーションの実装が完了したため、画面を開いて、やることに何も入力せずに追加ボタンを押してみましょう。
バリデーションエラーが発生するはずです。
また、maxLength
に20
を設定しているため、21文字以上入力して追加ボタンを押してもバリデーションエラーが発生します。
まとめ
今回は、サービス開発リファレンスを使ってバリデーションを実装しました。
次回はSPAにおけるファイルダウンロードをご紹介します。
本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。