投稿日
サービス開発リファレンスを使ってREST APIを1つ作成してみよう
はじめに
こんにちは。西日本テクノロジー&イノベーション室の齊藤です。
私の所属する西日本テクノロジー&イノベーション室では、2020年9月末に「SPA + REST API構成のサービス開発リファレンス(以下サービス開発リファレンス)」を公開しました。こちらは、シングルページアプリケーション(以下SPA)とREST APIから構成されるWebアプリケーションを開発する際に活用して頂けるコンテンツになっています。
前回のブログでは、SPAに絞ってタスク管理をするための簡易的な画面(Todo画面)の実装をご紹介しました。
今回はTodo一覧取得のREST APIを作り、前回作成したTodo画面とつなぎこんでみます。
- サービス開発リファレンスを使ってWebアプリケーションを作成してみよう<導入編>
- サービス開発リファレンスを使って1画面作成してみよう
- サービス開発リファレンスを使ってREST APIを1つ作成してみよう(←今回の記事)
事前準備
1~2までの記事でご紹介した作業の完成状態をベースに作成しますので、こちらの作業がまだの方は実施してみてください。
バックエンド(REST API)の作成
前回は以下のようなTodo画面を作成しました。
ここで表示している洗濯物を干す
、部屋を掃除する
というタスクは、ソースコードにハードコーディングしています。
今回はデータベースからタスクを取得するREST APIを作成し、REST APIから取得した結果をTodo画面に表示するようにします。
データベース周りの処理の実装
データベースには、PostgreSQLを使用します。
マイグレーションツールであるFlywayを使用して、データベース上のテーブルやテストデータを登録するようにします。
Flywayを使うことでデータベースの状態をバージョン管理でき、データベースに適用されているSQLを管理できたり、サーバを起動してすぐにアプリケーションを試せる状態になっていて素早く開発できるなどのメリットがあります。
Flywayについては、以下記事によくまとめられていますので、興味がある人は読んでみてください。
参考:Flyway使い方メモ
Flywayの実行については、Nablarchの初期化処理を利用して、バックエンドアプリを起動したタイミングで行うよう設定します(参考:Nablarch – オブジェクトの初期化処理を行う)
backend/src/main/resources/rest-component-configuration.xml
の以下コメントアウトを外します。
<component-ref name="dbMigration" />
次に、Todoの内容を扱うTODOテーブルを作成します。backend/src/main/resources/db/migration
パッケージを作成し、その配下にV1__create_table.sql
を作成します。ファイルには、TODOテーブルを作成するためのSQLを記述します。
backend/src/main/resources/db/migration/V1__create_table.sql
CREATE TABLE TODO
(
TODO_ID BIGINT NOT NULL,
TEXT VARCHAR(20) NOT NULL,
PRIMARY KEY (TODO_ID)
);
また、テスト実行時にデータベースにテストデータを投入するため、テスト用リソースディレクトリの backend/src/test/resources
に、db/testdata
ディレクトリを作成し、V9999__testdata.sql
ファイルを作成します。ファイルには、テストデータを登録するためのSQLを記述します。
backend/src/test/resources/db/testdata/V9999__testdata.sql
INSERT INTO TODO (TODO_ID, TEXT) VALUES (1, 'やること1');
INSERT INTO TODO (TODO_ID, TEXT) VALUES (2, 'やること2');
INSERT INTO TODO (TODO_ID, TEXT) VALUES (3, 'やること3');
Nablarchでは、データベースを操作するためにユニバーサルDAOという機能を使用します。
ユニバーサルDAOでは、エンティティと呼ばれるクラスを使用して、データベースの検索結果をマッピングします。
TODOテーブルは先ほど作成しているため、TODOテーブルに対応するTodoEntity
クラスを作成します。
backend/src/main/java/com/example/infrastructure/persistence/entity/TodoEntity.java
package com.example.infrastructure.persistence.entity;
import javax.persistence.*;
@Entity
@Table(name = "todo")
@Access(AccessType.FIELD)
public class TodoEntity {
@Id
private Long todoId;
private String text;
public Long getTodoId() {
return todoId;
}
public void setTodoId(Long todoId) {
this.todoId = todoId;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
これでデータベース周りの準備は完了しました。
アクションクラスの実装
次に、登録しているToDoを取得するためのREST APIを実装する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 javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
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());
}
public static class TodoResponse {
public final Long id;
public final String text;
public TodoResponse(Long id, String text) {
this.id = id;
this.text = text;
}
}
}
レスポンスのオブジェクトがJSONに変換されるため、TodoResponse
クラスを内部で定義し、データベースから取得したオブジェクトを変換します。また、java.util.List
で返すことで配列形式に変換されるため、TodoResponse
型のオブジェクトをListで返すことで、次のようなJSONに変換されます。
{
[
{
"id": 1,
"text": "やること1",
},
{
"id": 2,
"text": "やること2",
},
{
"id": 3,
"text": "やること3",
}
]
}
テストの実装
アクションクラスを作成してREST APIを定義したため、このREST APIが想定している動作をするのかテストします。
Nablarchでは、REST APIをテストするためテスティングフレームワークを提供していますので、それを使用してアプリサーバの起動およびREST APIの呼び出しをテストします。
backend/src/test/resources/unit-test.xml
の以下をコメントアウトしてください。
<component class="com.example.presentation.restapi.CsrfTokenManager" />
次に、テスト用Javaディレクトリのsrc/test/java
に、com.example.todo
パッケージを作成し、そこにRestApiTest
クラスを作成します。
backend/src/test/java/com/example/todo/RestApiTest.java
package com.example.todo;
import nablarch.fw.web.HttpResponse;
import nablarch.fw.web.RestMockHttpRequest;
import nablarch.test.core.http.SimpleRestTestSupport;
import org.junit.Test;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
public class RestApiTest extends SimpleRestTestSupport {
@Test
public void RESTAPIでToDo一覧が取得できる() {
RestMockHttpRequest request = get("/api/todos");
HttpResponse response = sendRequest(request);
assertStatusCode("ToDo一覧の取得", HttpResponse.Status.OK, response);
String responseBody = response.getBodyString();
assertThat(responseBody, hasJsonPath("$", hasSize(3)));
assertThat(responseBody, hasJsonPath("$[0].id", equalTo(1)));
assertThat(responseBody, hasJsonPath("$[0].text", equalTo("やること1")));
assertThat(responseBody, hasJsonPath("$[1].id", equalTo(2)));
assertThat(responseBody, hasJsonPath("$[1].text", equalTo("やること2")));
assertThat(responseBody, hasJsonPath("$[2].id", equalTo(3)));
assertThat(responseBody, hasJsonPath("$[2].text", equalTo("やること3")));
}
}
REST APIのテスティングフレームワークを使用するため、親クラスにnablarch.test.core.http.SimpleRestTestSupport
クラスを指定します。
合わせて、最初のテストとしてREST APIが想定しているパスとHTTPメソッドで呼び出せるかをテストします。
また、json-path-assertというライブラリを使用して、レスポンスとして返されたJSONに対して次の検証をします(JsonPathの記法について詳しくはドキュメントを参照してください)
- 配列の要素数が3つである
- 配列の1番目が、データの1番目の値と同じである
- 配列の2番目が、データの2番目と値と同じである
- 配列の3番目が、データの3番目と値と同じである
ここまで実装できたら、動作確認をしてみましょう。
以下コマンドでデータベースを起動します。
$ 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 test
BUILD SUCCESS
となっていれば成功です。
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.762 s - in com.example.todo.RestApiTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
フロントエンド(SPA)とのつなぎこみ
これでTodoを取得するREST APIの実装が完了しましたので、前回作成したTodo画面とつなぎこんでみましょう。
backend
ディレクトリで以下コマンドを実行して、バックエンドのアプリを起動しておきます。
$ mvn jetty:run
次に、SPA側で今回作成したAPIを呼び出す処理を実装します。
BackendService.ts
に以下を追加してください。
frontend/src/example/backend/BackendService.ts
const getTodos = async () => {
Logger.debug('call service of getTodos');
const response = await restClient.get('/api/todos');
Logger.debug(response);
return response.json();
};
また、export default
に上記で宣言したgetTodos
を追加します。
export default {
getHelloWorld,
getTodos,
};
ここまで実装すると、BackendService.ts
は次のようになっています。
import { RestClient, Logger } from 'framework';
const restClient = new RestClient();
const getHelloWorld = async () => {
Logger.debug('call service of getHelloWorld');
const response = await restClient.get('/api/helloWorld');
Logger.debug(response);
return response.json();
};
const getTodos = async () => {
Logger.debug('call service of getTodos');
const response = await restClient.get('/api/todos');
Logger.debug(response);
return response.json();
};
export default {
getHelloWorld,
getTodos,
};
次に、Todo.tsx
のuseEffect
で直にTodoを記載していた部分を以下のように修正し、REST APIからTodoの内容を取得するようにします。
frontend/src/example/components/pages/Todo.tsx
type Todo = {
id: number
text: string
}
~~~~~~~~~
useEffect(() => {
BackendService.getTodos()
.then(response => {
Logger.debug(response);
setTodos(response.map((todo: Todo) => todo.text));
});
}, []);
ここまで実装すると、Todo.tsx
は次のようになっています。
import React, {useEffect, useState} from 'react';
import { Logger, useInput } from "../../../framework";
import { BackendService } from '../../backend';
import './Todo.css';
type Todo = {
id: number
text: string
}
const Todo: React.FC = () => {
const [todos, setTodos] = useState<string[]>([]);
const [text, textAttributes] = useInput('');
useEffect(() => {
BackendService.getTodos()
.then(response => {
Logger.debug(response);
setTodos(response.map((todo: Todo) => todo.text));
});
}, []);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setTodos([...todos, text]);
};
return (
<div className="content">
<div className="form_field">
<form className="form" onSubmit={handleSubmit}>
<div className="input_field">
<input type="text" {...textAttributes} placeholder="やることを入力してください" />
</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;
これでSPA側の実装も完了したため、以下コマンドをfrontend
ディレクトリで実行してみましょう。
$ npm start
自動でブラウザが立ち上がり、Top画面が表示されているはずです。
今回はTodo画面を実装したため、以下URLを開いてみましょう。
http://localhost:3000/todo
データベースに入っている内容が画面に表示されていれば成功です。
まとめ
今回は、サービス開発リファレンスを使ってTodoを取得するREST APIを実装しました。
次回はバリデーションの機能をご紹介します。
本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。