【Spring】REST APIの作成②POSTで新規登録

### 概要


以下のような処理を非同期（JavaScriptのfetch）で行う登録機能を実装します。


1. 単語帳名をフォームに入力し「追加する」ボタンをクリック

2. バリデーション＆DB登録処理（Spring Boot側）を実行

3. 成功すれば、登録された単語帳名を一覧に即時反映
　(エラーがあればエラーメッセージを表示)

 ※spring securityを導入している前提
 
![restPost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4050514/ae24d0a1-cae8-4873-92be-d9fbbce4bd0e.png)





### ■ フロント側

SpringSecurityを導入しているため,POST,DELETE,PUTメソッドの送信にはCSRFトークンが必須になる。CSRFトークンの値はSpring Securityが自動でThymeleafに埋め込んで送信してくれるが、今回はJSでfetchする際にトークンの値を合わせて送信したいので、HTMLに手動で埋め込む。
#### html
```html
	<!-- 登録用フォーム -->
	<form th:action="@{/wordbooks/api/regist}" th:object="${wordbookForm}">
    	<label>単語帳名：<input type="text" th:field="*{wordbookName}"></label>
        <input type="hidden" id="csrfToken" th:value="${_csrf.token}" />
        <!-- バリデーションエラーメッセージ表示 -->
		<ul id="errorMsgList" class="errorMsgHidden"></ul>
        
		<button type="submit" id="registBtn">追加する</button>
	</form>
```
 #### JavaScript
```JavaScript
form.addEventListener("submit", async (event) => {
   
	event.preventDefault(); //formでのリクエスト送信をキャンセル
	errorMsgList.innerHTML = "";//エラーメッセージをいったん白紙に
    
    //各パラメータの値を取得
	const wordbookName = document.getElementById("wordbookName").value.trim();
    const csrfToken = document.getElementById("csrfToken").value;
    
	try {
		const res = await fetch(`/wordbooks/api/regist`, {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded",//フォームで送る      
				"X-CSRF-TOKEN": csrfToken//CSRFトークンもちゃんと送る
			},
			body: `wordbookName=${encodeURIComponent(wordbookName)}`
            //単語帳名の入力文字に"?"や"="が含まれていても入力文字として認識されるようエンコードする
		});
		if (res.ok) {
			const wordbook = await res.json();
            
			 // ここに単語帳一覧に表示する処理を記述する
             
		} else {
			const errorMessages = await res.json();

            //ここにエラーメッセージを表示する処理を記述する
		}
	} catch (error) {
		alert("登録に失敗しました");
	}
});

```

### ■ サーバ側(バリデーションクラス)
SpringのValidatorインタフェースを使い、DBに重複する単語帳名が存在するかチェックする。
```java
@Component
@RequiredArgsConstructor
public class WordbookValidator implements Validator{
	private final WordbookService wordbookService;

	@Override
	public boolean supports(Class<?> clazz) {
		return WordbookForm.class.isAssignableFrom(clazz);
	}

	@Override
	public void validate(Object target, Errors errors) {
		WordbookForm wordbookForm = (WordbookForm) target;
        //単語帳名でDB検索
		Optional<Wordbook> wordbookOpt = wordbookService.findByWordbookName(wordbookForm.getWordbookName());
        //存在した場合はエラー発生
		if(wordbookOpt.isPresent()) {
			errors.rejectValue("wordbookName", null, "この単語帳名は既に登録されています");
		}			
	}
}
```

### ■ サーバ側(コントローラ)
ResponseEntityクラスを使うと、HTTPレスポンスのステータスコード・ヘッダー・ボディを自由にカスタマイズできる。フロント側で更新処理の成功or失敗で挙動を分けたい場合、サーバ側(コントローラ)はステータスコードを明示的に指定してレスポンスを返してあげればよい。

```Java
@RestController
@RequiredArgsConstructor
public class WordbookApiController {
	
	private final WordbookService wordbookService;
    private final WordbookValidator wordbookValidator;
    
	//自作のバリデータをバインド
	@InitBinder("wordbookForm")
	public void initBinder(WebDataBinder binder) {
		binder.addValidators(wordbookValidator);
	}
	
	@PostMapping("/regist")
	public ResponseEntity<?> registWordBook(
			@AuthenticationPrincipal LoginUserDetails loginUserDetails,
			@Validated WordbookForm wordbookForm,
			BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
	        // エラーメッセージのリストを作成
	        List<String> errorList = bindingResult.getAllErrors().stream()
	            .map(error -> error.getDefaultMessage())
	            .collect(Collectors.toList());
	        // 400 Bad Request でエラーリストを返す
	        return ResponseEntity.badRequest().body(errorList);
	    }
		//登録処理
		Wordbook wordbook = new Wordbook();
		wordbook.setUser(loginUserDetails.getUser());
		wordbook.setName(wordbookForm.getWordbookName());
		WordbookDto dto = wordbookService.save(wordbook);
		return ResponseEntity.ok(dto);
	}
```
### ■ クライアント側(JS)でJSONを取得する
```JavaScript
//fetch処理の続き
	if (res.ok) {
			const wordbook = await res.json();       
            //一覧リストに追加する関数
			addWordbookToList(wordbook.wordbookName);
		} else {		
			//バリデーションエラーがあるとき
			errorMsgList.classList.replace("errorMsgHidden","errorMsgVisible");
			const errorMessages = await res.json();
			for(const msg of errorMessages){
				const li = document.createElement("li");
				li.textContent = msg;
				errorMsgList.append(li);
			} 
		}
	} catch (error) {
		alert("登録に失敗しました");
	}
})
```