【Spring Boot + MyBatis】はじめてREST APIを作成してみる

## はじめに
3年目で転職活動をしていた時、「APIを作成したことはあるか」と聞かれたことがありました。
実務経験も個人開発の経験もなく、Java/SpringBootで作ってみたいと思ったのがきっかけです。

まずUdemyで学習を行いました。
[Web API 開発入門：Spring Boot と OpenAPI で始めるスキーマ駆動開発](https://www.udemy.com/course/poco-tech-spring-boot-web-api)
特にAPIの設計は初めてだったので勉強になりました。
この教材はIntelliJ、Gradleだったので、使い慣れたEclipse、Mavenで作成してみたのが今回の記事になります。

## 設計
| 機能 | HTTPメソッド | エンドポイント | ステータスコード |
| --- | --- | --- | --- |
| 一覧取得 | GET | /manual-api | 200 OK<br>400 Bad Request |
| 詳細取得 | GET | /manual-api/{id} | 200 OK<br>404 Not Found |
| 登録 | POST | /manual-api | 201 Created<br>400 Bad Request |
| 更新 | PUT | /manual-api/{id} | 200 OK<br>400 Bad Request<br>404 Not Found |
| 削除 | DELETE | /manual-api/{id} | 204 No Content<br>404 Not Found |

## 実装例
:::
```ManualController.java
@RestController
public class ManualController {

	@Autowired
	ManualsMapper manualsMapper;

    // 一覧取得
    @GetMapping("/manual-api")
    public ResponseEntity<List<Manuals>> getList() {
        List<Manuals> manualList = manualsMapper.getList();
        return ResponseEntity.ok(manualList);
    }

    // 詳細取得
    @GetMapping("/manual-api/{manualId}")
    public ResponseEntity<Manuals> getDetail(@PathVariable int manualId) {
        Manuals manual = manualsMapper.getDetail(manualId);
        return ResponseEntity.ok(manual);
    }

    // 登録
    @PostMapping("/manual-api")
    public ResponseEntity<Manuals> create(@RequestBody Manuals manual) {
    	// 登録
    	manualsMapper.create(manual);
        return ResponseEntity.ok(manual);
    }

    // 更新
    @PutMapping("/manual-api/{manualId}")
    public ResponseEntity<Manuals> update(@PathVariable int manualId, @RequestBody Manuals manual) {
    	// マニュアルID
    	manual.setManualId(manualId);
    	// 更新
    	manualsMapper.update(manual);
        return ResponseEntity.ok(manual);
    }

    // 削除
    @DeleteMapping("/manual-api/{manualId}")
    public ResponseEntity<Void> delete(@PathVariable int manualId) {
    	manualsMapper.delete(manualId);
        return ResponseEntity.noContent().build();
    }
}
```

```Manuals.java
@Data
public class Manuals {

	// マニュアルID
	private int manualId;

	// 社員ID
	private int userId;

	// 表示順
	private int displayOrder;

	// タイトル
	private String title;

	// 掲載開始日
	private LocalDate startDate;

	// 掲載終了日
	private LocalDate endDate;

	// 内容
	private String content;

	// リンク
	private String link;

	// レコード登録者
    private String createdBy;

    // レコード登録日
    private LocalDateTime createdAt;

    // レコード更新者
    private String updatedBy;

    // レコード更新日
    private LocalDateTime updatedAt;
}
```

```ManualsMapper.java
@Mapper
public interface ManualsMapper {

	// 一覧取得
    public List<Manuals> getList();

    // 詳細取得
    public Manuals getDetail(int manualId);

    // 登録
    public void create(Manuals manual);

    // 更新
    public void update(Manuals manual);

    // 削除
    public void delete(int manualId);
}
```

```ManualsMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.manualapi.ManualsMapper">

	<!-- 一覧取得 -->
    <select id="getList" resultMap="ManualMap">
        SELECT * FROM manuals
    </select>

 	<!-- 詳細取得 -->
    <select id="getDetail" resultMap="ManualMap">
        SELECT * FROM manuals
        WHERE manual_id = #{manualId}
    </select>

	<!-- マッピング -->
    <resultMap id="ManualMap" type="com.manualapi.Manuals">
        <result property="manualId" column="manual_id" />
        <result property="userId" column="user_id" />
        <result property="displayOrder" column="display_order" />
        <result property="title" column="title" />
		<result property="startDate" column="start_date" />
		<result property="endDate" column="end_date" />
		<result property="content" column="content" />
		<result property="link" column="link" />
        <result property="createdBy" column="created_by" />
        <result property="createdAt" column="created_at" />
        <result property="updatedBy" column="updated_by" />
        <result property="updatedAt" column="updated_at" />
    </resultMap>

	<!-- 登録 -->
    <insert id="create" useGeneratedKeys="true" keyProperty="manualId">
		INSERT INTO manuals
		(
			user_id,
			display_order,
			title,
			start_date,
			end_date,
			content,
			link,
			created_by,
			created_at
		)
		VALUES
		(
			#{userId},
			#{displayOrder},
			#{title},
			#{startDate},
			#{endDate},
			#{content},
			#{link},
			#{createdBy},
			CURRENT_TIMESTAMP
		)
    </insert>

    <!-- 更新 -->
    <update id="update">
		UPDATE manuals SET
    		user_id = #{userId},
			display_order = #{displayOrder},
			title = #{title},
			start_date = #{startDate},
			end_date = #{endDate},
    		content = #{content},
			link = #{link},
			updated_by = #{updatedBy},
			updated_at = CURRENT_TIMESTAMP
    	WHERE
			manual_id = #{manualId}
    </update>

	<!-- 削除 -->
	<delete id="delete">
		DELETE FROM manuals
		WHERE
			manual_id = #{manualId}
    </delete>
</mapper>
```
application.propertiesのポートを8081にします。
（アプリケーションが8080を使用するため）
```application.properties
# DB設定値は省略
server.port=8081
```
入力チェックははじめAPI側に書いていましたが、画面側プロジェクトで行うようにしたため記載はありません。
また、画面入力値をエンティティへのセットするのも画面側プロジェクトで行うようにしました。

## エラーハンドリング
```CustomControllerAdvice.java
@RestControllerAdvice
public class CustomControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({ HttpMessageNotReadableException.class, MethodArgumentNotValidException.class })
    public Map<String, Object> handleError400() {
    	Map<String, Object> errorMap = new HashMap<String, Object>();
    	errorMap.put("message", "リクエストが正しくありません。");
    	errorMap.put("status", HttpStatus.BAD_REQUEST);
		return errorMap;
    }

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler({ NoResourceFoundException.class })
    public Map<String, Object> handleError404() {
    	Map<String, Object> errorMap = new HashMap<String, Object>();
    	errorMap.put("message", "該当するエンドポイントがありませんでした。");
    	errorMap.put("status", HttpStatus.NOT_FOUND);
		return errorMap;
    }
}
```

## 動作確認
Postmanが有名ですが、Postmanの不具合で立ち上がらなかったため、Chrome拡張機能のTalend API Testerを使用しました。
Postmanと同じように使えました。
![スクリーンショット 2024-04-14 11.41.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/decc901a-6a8d-f519-9077-c9752f91df45.png)

![スクリーンショット 2024-04-14 11.39.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/212cc6d9-83c6-955a-2170-0d296d40b8b3.png)

## さいごに
記事を書くにあたって改めて見てみて、設計書通り作成できてなかったことに気づき修正しました。
初心者向けに分かりやすく説明したものではなく、動作確認できたものを羅列するだけになってしまったため、説明できるように理解を深めていきたいです。

## 参考記事
[【頑張れば5分で作れる】Spring Boot + MyBatisで超簡単なCRUD処理のREST APIを作る【VSCode】](https://qiita.com/Toshihiro_Watanabe_/items/55ad3d3a6393b88c6fcf)
[SpringBootに入門するための助走本（Zenn改訂版）Chapter 04 簡単なWebAPIを作ってみよう編](https://zenn.dev/sugaryo/books/spring-boot-run-up/viewer/api_controller)