エラーハンドリング — Spring Boot側でエラーを返してNext.jsで表示する
はじめに
APIを作ると必ず必要になるのがエラーハンドリングです。Spring Boot側で適切なエラーレスポンスを返し、Next.js側でそれをユーザーにわかりやすく表示するまでの流れを整理します。
Spring Boot側:エラーレスポンスの統一
バラバラなエラー形式はフロントエンドが扱いにくいため、共通のエラーレスポンス構造を定義します。
// エラーレスポンスDTO
public class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
this.timestamp = LocalDateTime.now();
}
// getter省略
}
カスタム例外クラス
HTTPステータスに対応した例外クラスを定義します。
// 404用
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// 400用
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
@RestControllerAdvice — 例外をまとめてハンドリング
@RestControllerAdvice
public class GlobalExceptionHandler {
// 404: リソースが見つからない
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
// 400: リクエストが不正
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequest(BadRequestException ex) {
ErrorResponse error = new ErrorResponse(400, ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
// 401: 認証エラー
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ErrorResponse> handleUnauthorized(UnauthorizedException ex) {
ErrorResponse error = new ErrorResponse(401, ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
// 500: 予期せぬエラー(全体のフォールバック)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(500, "サーバーエラーが発生しました");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Service層での例外スロー
@Service
public class UserService {
public UserResponse findById(Long id) {
return userRepository.findById(id)
.map(UserResponse::new)
.orElseThrow(() -> new ResourceNotFoundException(
"ユーザーが見つかりません: id=" + id
));
}
}
エラーレスポンスのJSON例:
{
"status": 404,
"message": "ユーザーが見つかりません: id=99",
"timestamp": "2026-07-30T10:00:00"
}
Next.js側:エラーを受け取って表示
Server Componentでのエラーハンドリング
// app/users/[id]/page.tsx
export default async function UserDetailPage({
params,
}: {
params: { id: string };
}) {
const res = await fetch(
`${process.env.API_URL}/api/users/${params.id}`
);
if (!res.ok) {
const error = await res.json();
if (res.status === 404) {
notFound(); // Next.jsのnot-found.tsxを表示
}
throw new Error(error.message); // error.tsxにフォールバック
}
const user = await res.json();
return <div>{user.name}</div>;
}
not-found.tsx — 404専用ページ
// app/users/[id]/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>ユーザーが見つかりません</h2>
<a href="/users">一覧に戻る</a>
</div>
);
}
error.tsx — 予期せぬエラーのフォールバック
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>エラーが発生しました</h2>
<p>{error.message}</p>
<button onClick={reset}>再試行</button>
</div>
);
}
Server Actionsでのエラーハンドリング
// useActionStateでエラーメッセージをフォームに返す
"use server";
export async function createUser(
prevState: { error: string | null },
formData: FormData
) {
const res = await fetch(`${process.env.API_URL}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: formData.get("name"),
email: formData.get("email"),
}),
});
if (!res.ok) {
const error = await res.json();
return { error: error.message }; // フォームにエラーを返す
}
redirect("/users");
}
まとめ
| レイヤー | 実装 | 役割 |
|---|---|---|
| Spring Boot | カスタム例外クラス | HTTPステータスに対応した例外を定義 |
| Spring Boot | @RestControllerAdvice |
例外をまとめてキャッチしてJSON返却 |
| Next.js | notFound() |
404をnot-found.tsxに転送 |
| Next.js | error.tsx |
予期せぬエラーのUI表示 |
| Next.js | Server Actions | フォームエラーをstateで返す |
次回は DBスキーマ設計とJPA — Entity・DTO・Repository・Serviceの関係を整理します。