AmplifyでホスティングしたAngularのアプリにAPIを追加してみた

2021.06.01

こんにちは!DA(データアナリティクス)事業本部 インテグレーション部の大高です。

先日、Amplifyの入門としてAngularアプリをホスティングして、実際にブラウザで表示するところを試してみました。

今回、更にAPIを作成してアプリから利用するところを試してみたいと思います。

前提

この環境は以下の記事のとおりAmplifyを利用したAngularのアプリを作成し、AWS上にホスティングしています。

今回やりたいこと

Angularアプリ上に簡単なAPI操作を組み込ませたいです。

今回は「現在の天気の情報」を画面上に表示させ、ボタンをクリックすることで最新の情報に更新できるようにしたいと思います。具体的には以下を満たすアプリとしたいと思います。

  • 初期画面の表示時、および、更新ボタンをクリックすることで天気情報を取得する
  • 取得した天気情報をデータベースに登録する
  • 登録した天気情報はデータベースから取得して画面に即時反映させる

なお、天気情報をわざわざデータベースに登録する意味はないのですが、APIを組み込みたいので処理として入れています。また、天気情報については、下記の「OpenWeatherMap」のAPIを利用して、現在の「東京都」の天気を対象として取得したいと思います。

Amplifyまわりの実装については、下記のチュートリアルを参考にすすめていきます。

アプリの実装

では、アプリを実装していきます!

天気情報 取得用サービスの追加

まずは、天気情報を取得するためのサービスを追加します。試しに以下のようにOpenWeatherMapのAPIを実行して、レスポンスを取得してみます。

https://api.openweathermap.org/data/2.5/weather?q=Tokyo&units=metric&lang=ja&appid=XXXXX

取得したレスポンスから、interfaceを定義します。今回は下記を利用して自動生成してみました。

生成したinterfaceのコードはng generateコマンドで作成した.tsファイルに転記しておきます。

$ ng generate interface current-weather

src/app/current-weather.ts

export interface CurrentWeather {
  coord: Coord;
  weather: Weather[];
  base: string;
  main: Main;
  visibility: number;
  wind: Wind;
  clouds: Clouds;
  dt: number;
  sys: Sys;
  timezone: number;
  id: number;
  name: string;
  cod: number;
}

export interface Clouds {
  all: number;
}

export interface Coord {
  lon: number;
  lat: number;
}

export interface Main {
  temp: number;
  feels_like: number;
  temp_min: number;
  temp_max: number;
  pressure: number;
  humidity: number;
}

export interface Sys {
  type: number;
  id: number;
  country: string;
  sunrise: number;
  sunset: number;
}

export interface Weather {
  id: number;
  main: string;
  description: string;
  icon: string;
}

export interface Wind {
  speed: number;
  deg: number;
  gust: number;
}

次に、本命のサービスを作成します。

$ ng generate service weather

environmentファイルにOpenWeatherMapのAPI情報を記載しつつ、ざっくり以下のように作成してみました。

src/app/weather.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { CurrentWeather } from './current-weather';

@Injectable({
  providedIn: 'root',
})
export class WeatherService {
  apiUrl: string;

  constructor(private http: HttpClient) {
    const key = environment.weatherApi.key;
    const endpoint = environment.weatherApi.current.endpoint;
    const query = environment.weatherApi.current.query;
    this.apiUrl = `${endpoint}?appid=${key}&${query}`;
  }

  getCurrentWeather() {
    return this.http.get<CurrentWeather>(this.apiUrl);
  }

  formatWeatherInfo(response: CurrentWeather) {
    const city = response.name;
    const description = response.weather[0].description;
    const temp = response.main.temp;
    const message = `${city} の現在の天気は ${description} で、気温は ${temp} 度です。`;

    return message;
  }
}

environment.tsは以下のような感じです。クエリパラメータはとりあえず固定にしています。

src/environments/environment.ts

export const environment = {
  production: false,
  weatherApi: {
    key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
    current: {
      endpoint: 'https://api.openweathermap.org/data/2.5/weather',
      query: 'q=Tokyo&units=metric&lang=ja',
    },
  },
};

あとはapp.componentを以下のようにして、簡単に画面に天気情報を表示できるようにします。また詳細な記述は省きますが、Google Fontsを利用してアイコン表示をしたり、app.module.tsHttpClientModuleの追加もしています。

src/app/app.component.ts

import { Component } from '@angular/core';
import { WeatherService } from 'src/app/weather.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'hello-amplify-angular';
  updateTimestamp = '';
  currentWeather = '';

  constructor(private weatherService: WeatherService) {}

  ngOnInit() {
    this.updateWeatherInfo();
  }

  update() {
    this.updateWeatherInfo();
  }

  updateWeatherInfo() {
    this.weatherService.getCurrentWeather().subscribe((weather) => {
      this.updateTimestamp = new Date().toLocaleString();
      this.currentWeather = this.weatherService.formatWeatherInfo(weather);
    });
  }
}

src/app/app.component.html

(snip...)
  <!-- Weather Info -->
  <h2>Weather Info</h2>
  <div class="card-container">
    <div class="circle-link" (click)="update()">
      <span class="material-icons">update</span>
    </div>
    <div class="card" style="width: 100%">
      [{{ updateTimestamp }}] {{ currentWeather }}
    </div>
  </div>
(snip...)

これで大枠はできました。これだと単純にOpenWeatherMap APIを利用して天気情報を取得・表示しているだけなので、ここからAmplify APIと絡めていきましょう。

AmplifyのAPIを追加する

amplify add apiコマンドでAmplifyのAPIを追加します。コマンドを実行すると、設定情報を聞かれるのでそれぞれ回答していきます。

$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: HelloAmplifyAngularApi
? Choose the default authorization type for the API API key
? Enter a description for the API key: hello-amplify-angular-key
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)

The following types do not have '@auth' enabled. Consider using @auth with @model
         - Todo
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/auth


GraphQL schema compiled successfully.

Edit your schema at /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema.graphql or place .graphql files in a directory at /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema
? Do you want to edit the schema now? Yes
Edit the file in your editor: /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema.graphql
Successfully added resource HelloAmplifyAngularApi locally

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

コマンドの実行時に、以下のように編集すべきスキーマファイルを「いま編集する」と回答するとファイルを開いてくれるので、該当ファイルを編集します。

? Do you want to edit the schema now? Yes
Edit the file in your editor: /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema.graphql

デフォルトのTodoモデルを修正して以下のようにします。id以外にはcitydescriptiontemperatureカラムをもつようにしてみました。

amplify/backend/api/HelloAmplifyAngularApi/schema.graphql

type CurrentWeather @model {
  id: ID!
  city: String!
  description: String
  temperature: String
}

API(バックエンド)をデプロイする

では、追加したAPIをデプロイします。

デプロイはamplify pushコマンドで実施します。プロンプトの質問ですが、今回は全部Y or デフォルト値 で回答して進めます。

$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category | Resource name          | Operation | Provider plugin   |
| -------- | ---------------------- | --------- | ----------------- |
| Api      | HelloAmplifyAngularApi | Create    | awscloudformation |
| Hosting  | amplifyhosting         | No Change |                   |
? Are you sure you want to continue? Yes

The following types do not have '@auth' enabled. Consider using @auth with @model
         - CurrentWeather
Learn more about @auth here: https://docs.amplify.aws/cli/graphql-transformer/auth


GraphQL schema compiled successfully.

Edit your schema at /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema.graphql or place .graphql files in a directory at /xxxxx/hello-amplify-angular/amplify/backend/api/HelloAmplifyAngularApi/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target angular
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.graphql
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/app/API.service.ts
⠴ Updating resources in the cloud. This may take a few minutes...

しばらく待つと、以下のように完了した旨が通知されます。

✔ Generated GraphQL operations successfully and saved at src/graphql
✔ Code generated successfully and saved in file src/app/API.service.ts
✔ All resources are updated in the cloud

GraphQL endpoint: https://xxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: xxxxx

APIを組み込む

それでは、追加したAPIをアプリに組み込んでいきます。

API組み込みのための準備

まず先に必要なAmplifyのライブラリを以下のとおり追加しておきます。また、GraphQL用のライブラリも追加します。

$ yarn add aws-amplify @aws-amplify/ui-angular
$ yarn add --dev graphql

また、APIの組み込みの際にaws-exports.jsを読み込む必要があるのですが、デフォルトだとjsファイルの読み込みができません。そのため、対応としてtsconfig.jsoncompilerOptionsallowJs設定を追加して、jsファイルを読み込めるようにします。

tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    ...snip...
    "allowJs": true
  },
  ...snip...
}

この上で、main.tsでAmplifyの設定処理を追記しておきます。

src/main.ts

(snip...)
import Amplify from 'aws-amplify';
import awsmobile from './aws-exports';
Amplify.configure(awsmobile)
(snip...)

また、polyfills.tsには下記の設定追加が必要です。

src/polyfills.ts

(snip...)
 (window as any).global = window;
 (window as any).process = {
   env: { DEBUG: undefined },
 };

これで一通りの準備ができました。

APIの組み込み

APIの利用のために、GraphQLのスキーマに合わせたtypeを定義します。ファイルは新規に手動で作成しました。

src/types/current-weather-gq.ts

export type CurrentWeatherGq = {
  id: string;
  city: string;
  description: string;
  temperature: string;
};

次に、作成済みのweather.service.tsを修正して処理を組み込んでいきます。registCurrentWeatherを追加し、データを登録処理を実装します。また、formatWeatherInfoはGraphQL用のオブジェクトから情報を取得するように変更します。

src/app/weather.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { CurrentWeatherGq } from '../types/current-weather-gq';
import { APIService } from './API.service';
import { CurrentWeather } from './current-weather';

@Injectable({
  providedIn: 'root',
})
export class WeatherService {
  apiUrl: string;

  constructor(private api: APIService, private http: HttpClient) {
    const key = environment.weatherApi.key;
    const endpoint = environment.weatherApi.current.endpoint;
    const query = environment.weatherApi.current.query;
    this.apiUrl = `${endpoint}?appid=${key}&${query}`;
  }

  getCurrentWeather() {
    return this.http.get<CurrentWeather>(this.apiUrl);
  }

  registCurrentWeather(currentWeather: CurrentWeatherGq) {
    this.api.CreateCurrentWeather(currentWeather);
  }

  formatWeatherInfo(currentWeatherGq: CurrentWeatherGq) {
    const city = currentWeatherGq.city;
    const description = currentWeatherGq.description;
    const temp = currentWeatherGq.temperature;
    const message = `${city} の現在の天気は ${description} で、気温は ${temp} 度です。`;

    return message;
  }
}

サービスができたらapp.component.tsも修正します。

subscribeWeatherInfoによって、GraphQLのレコードが登録されたら、画面上に反映されるようにします。また、registWeatherInfoではOpenWeatherMap APIで取得したデータをGraphQLのレコードとして登録しています。これにより、レコード登録後にすぐに登録されたデータが画面上に反映されるようになります。

src/app/app.component.ts

import { Component } from '@angular/core';
import { WeatherService } from 'src/app/weather.service';
import { CurrentWeatherGq } from '../types/current-weather-gq';
import { APIService } from './API.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'hello-amplify-angular';
  updateTimestamp = '';
  currentWeather = '';

  constructor(
    private api: APIService,
    private weatherService: WeatherService
  ) {}

  ngOnInit() {
    this.subscribeWeatherInfo();
    this.registWeatherInfo();
  }

  update() {
    this.registWeatherInfo();
  }

  subscribeWeatherInfo() {
    this.api.OnCreateCurrentWeatherListener.subscribe((event: any) => {
      const newCurrentWeather = event.value.data.onCreateCurrentWeather;
      this.currentWeather =
        this.weatherService.formatWeatherInfo(newCurrentWeather);
    });
  }

  registWeatherInfo() {
    this.weatherService.getCurrentWeather().subscribe((weather) => {
      this.updateTimestamp = new Date().toLocaleString();

      const currentWeatherGq: CurrentWeatherGq = {
        id: this.updateTimestamp,
        city: weather.name,
        description: weather.weather[0].description,
        temperature: weather.main.temp.toString(),
      };

      this.weatherService.registCurrentWeather(currentWeatherGq);
    });
  }
}

動かしてみる

実装ができたら、ローカル環境で動かしてみましょう。

$ yarn start

想定どおり動いていることが確認できるかと思います。また、DynamoDBも確認してみましたが、想定どおりレコードが登録されていました。

動作に問題なければ、git上にpushしてフロントエンド側もデプロイ完了です。

はまったこと

今回ちょっとはまったのは、下記のエラーです。

Error: node_modules/@aws-amplify/api-graphql/lib-esm/types/index.d.ts:1:30 - error TS7016: Could not find a declaration file for module 'graphql/error/GraphQLError'. '/xxxxx/hello-amplify-angular/node_modules/graphql/error/GraphQLError.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/graphql` if it exists or add a new declaration (.d.ts) file containing `declare module 'graphql/error/GraphQLError';`

@types/graphqlを追加せよということですが、実際には下記のとおりにgraphqlをインストールすることでエラーが解消しました。

$ yarn add --dev graphql

まとめ

以上、AmplifyでホスティングしたAngularのアプリにAPIを追加してみました。

OpenWeatherMapを利用することで、当初思っていたよりも試行錯誤が多くて少々実装に悩みましたが、最終的にやりたかったAmplifyによるAPI実装はとてもシンプルでわかりやすいと感じました。また、自動でAPI操作に必要なメソッドも構築されるのでとても便利ですね。

今後は、機会があればAPIとフロントエンドにCognito認証をつけて試してみたいと思います。

どなたかのお役に立てば幸いです。それでは!

参考