【Node.js】ExpressとOpenWeatherMapAPIを用いて天気予報を取得してみた

## はじめに

  UdemyのWeb Developer BootcampでNode.jsやExpressの学習を進めていた中で、試験的にWeb APIも使用して、
  簡易的な現在の天気を検索するアプリを作成してみました。

## 概要
  今回作成したのは、**OpenWeatherAPI** という気象情報を取得できるWeb APIを使用しました。  
  URLは[コチラ](https://openweathermap.org/)から。  
  ユーザー登録が済めばすぐにAPIキーの取得が可能です。

  :::note warn
APIキーは有効化されるまで少し時間がかかるので注意してください。私は約20分ほどかかりました。
:::

  無料版と有料版で、取得できる回数や日数が変わってきますが、今回は無料版を使用するため、現在の天気を取得していきます。（無料版APIでも1分間に60リクエストかつ1ヶ月100万リクエスト可能らしい）  
  検索ページで都市名を入力すると、その地域の気象情報を取得できる形にします。

## Node.js と Express
  **Node.js** は、JavaScriptをサーバーサイドで実行するためのランタイム環境です。  
  Node.jsを使えば、クライアントサイドだけでなく、サーバーサイドもJavaScriptを使用してアプリケーション開発が可能です。  

  **Express** は、Node.jsの上に構築された軽量なWebアプリケーションフレームワークです。Expressを使うことで、Node.jsのみだと補いきれないような、ルーティングやミドルウェアといった便利な機能が簡単に追加されます。  
  例えば、リクエストを受け付けるサーバーの起動や、HTTPレスポンスと関連コンテンツの作成が可能です。

## フォルダ構成
  フォルダ構成は下記になります。
```
/weatherSearch
    /views
        index.ejs
        weather.ejs
        error.ejs
    app.js
    package.json

```

## 実際のコード
  実際のコードは下記になります。 

### ① app.js
  まずは、ユーザーがフォームに都市名を入力すると、その都市の天気情報を取得して結果を返すコードです。  
  Expressを使用して実装しています。
```javascript:app.js
const express = require('express');
const axios = require('axios');
const app = express();
const port = 8080;

// OpenWeatherMapのAPIキー
const apiKey = 'ここには取得したAPIキーを入力してください';

// テンプレートエンジンとしてEJSを使用（HTMLファイルの代わりに利用する）
app.set('view engine', 'ejs');

// POSTリクエストのbodyを解析するために必要になる
app.use(express.urlencoded({ extended: true }));

// ルートページにアクセスすると検索フォームを表示
app.get('/', (req, res) => {
  res.render('index'); // views/index.ejs を表示
});

// フォーム送信後、天気情報を取得して表示
app.post('/weather', (req, res) => {
  const city = req.body.city; // フォームから送信された都市名を取得
  const url = `http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&lang=ja&units=metric`;

  axios.get(url)
    .then(response => {
      const weatherData = {
        city: response.data.name,
        weather: response.data.weather[0].description,
        temp: response.data.main.temp,
        windspeed: response.data.wind.speed
      };
      res.render('weather', { weather: weatherData }); // 取得した天気情報をweather.ejsで表示
    })
    .catch(error => {
      console.error("エラーが発生しました。", error);
      res.render('error', { errorMessage: '天気情報を取得できませんでした。' });
    });
});

// サーバーを起動
app.listen(port, () => {
  console.log(`サーバーがポート${port}で起動しました`);
});
```

### ② index.ejs
  検索画面です。  
  今回は、テンプレートエンジンの1つである **EJS（Embedded JavaScript）テンプレート**を使って動的に内容を表示させています。

```html:index.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天気検索</title>
</head>
<body>
    <h1>天気を知りたい都市を入力してください</h1>
    <form action="/weather" method="POST">
        <label for="city">都市名:</label>
        <input type="text" id="city" name="city" required>
        <button type="submit">検索</button>
    </form>
</body>
</html>
```

### ③ weather.ejs
  結果表示画面です。
```html:weather.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天気情報</title>
</head>
<body>
    <h1><%= weather.city %>の天気情報</h1>
    <p>天気: <%= weather.weather %></p>
    <p>温度: <%= weather.temp %>°C</p>
    <p>風速: <%= weather.windspeed %>m/s</p>
    <a href="/">戻る</a>
</body>
</html>
```

### ④ error.ejs
  エラー発生時の画面になります。
```html:error.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>エラー</title>
</head>
<body>
    <h1>エラーが発生しました</h1>
    <p><%= errorMessage %></p>
    <a href="/">戻る</a>
</body>
</html>
```

## コード解説

  上記コードについて解説します。

### ① app.js
1. **モジュールの読み込み**
    ```javascript
    const express = require('express');
    const axios = require('axios');
    ```
    - **express** : Node.js用のWebフレームワークで、簡単にWebサーバーを構築できるようになります。この行でexpressモジュールを読み込んでいます。
    - **axios** : HTTPリクエストを送るためのライブラリで、ここでは、OpenWeatherMap APIへのリクエストに使われます。

    ちなみに、ここで**require**というメソッドが使われていますが、これは**モジュールをインポートするための関数**です。  

    ここでは詳細の説明は省略しますが、Node.jsは**モジュールシステム**を持っており、requireを使うことで、他のファイルや外部ライブラリで定義された機能（関数、オブジェクト、クラスなど）を現在のファイルに読み込むことができます。  

    例えば、**NPM（Node Package Manager）** は、Node.jsのパッケージ管理ツールです。（今回も使用しています）  
    NPMを使用すると、外部ライブラリやモジュールを簡単にプロジェクトにインストールし、requireを使ってそのライブラリをプロジェクトで使用することができます。  

    #### package.jsonについて
    パッケージ構成の中に、**package.json** というファイルがありました。Node.jsの環境構築ができていれば、
    ```
    npm init
    ```
    とコマンドで入力すると、package.jsonファイルが作成されます。  
    このファイルには、プロジェクトの依存関係やスクリプトなどが記述されるのですが、
    ```
    npm install express
    npm install axios
    ```
    というように、使用したいモジュールをインストールすると、**node_modulesディレクトリ** が作成され、その中にexpressパッケージとaxiosパッケージがダウンロードされます。  
    また、**package.json** の **dependencies**に**expressとaxios** が追加されます。

    参考までに、下記のような形です。

    ```json:package.json
    {
        "name": "weathersearch",
        "version": "1.0.0",
        "main": "index.js",
        "scripts": {
            "test": "echo \"Error: no test specified\" && exit 1"
        },
        "author": "",
        "license": "ISC",
        "description": "",
        "dependencies": {
            "axios": "^1.7.7",
            "ejs": "^3.1.10",
            "express": "^4.21.0"
        }
    }
    ```

2. **Expressアプリケーションの作成とポート設定**
    ```javascript
    const app = express();
    const port = 8080;
    ```
    - **app** : Expressのアプリケーションオブジェクトで、ルーティング（HTTPリクエストを処理するためのエンドポイント（URLとHTTPメソッド）を定義し）やミドルウェアの設定に使用します。  

    - **port** : ポート番号を8080に設定。ここで指定したポートでクライアントからのリクエストを受け付けます。

3. **OpenWeatherMapのAPIキーの設定**
    ```javascript
    const apiKey = 'ここには取得したAPIキーを入力してください';
    ```
    - **apiKey** : OpenWeatherMap APIにアクセスするためのAPIキー。このAPIキーを使用して認証を行い、天気情報を取得します。
:::note warn
上述しましたが、APIキーは有効化されるまで少し時間がかかるので注意してください。私は大体20分くらいかかりました。有効化してから実行しないとエラーが発生します。 
:::

4. **テンプレートエンジンの設定**
    ```javascript
    app.set('view engine', 'ejs');
    ```
    - **app.set('view engine', 'ejs')** : ExpressでテンプレートエンジンとしてEJSを使用することを指定しています。setを使用することでこの後に登場するres.renderメソッドでEJSファイルをレンダリングできるようになります。

5. **POSTリクエストのbody解析**
    ```javascript
    app.use(express.urlencoded({ extended: true }));
    ```
    - **app.use(express.urlencoded({ extended: true }))** : フォームから送信されたデータ（POSTリクエストのbody）を解析するためのコードです。POSTリクエストやPUTリクエストなど、HTTPリクエストのボディに含まれるデータ（例えば、フォームの送信データ）を解析することが可能となります。

6. **ルートパスへのGETリクエストハンドリング**
    ```javascript
    app.get('/', (req, res) => {
    res.render('index'); // views/index.ejs を表示
    });
    ```
    - **app.get('/', (req, res))** : /ルート（今回で言うとlocalhost:8080）にアクセスされたときのGETリクエストを処理します。  

    - **res.render('index')** : viewsフォルダ内のindex.ejsテンプレートをレンダリングして表示します。  

7. **天気情報を取得するPOSTリクエストの処理**
    ```javascript
    app.post('/weather', (req, res) => {
    const city = req.body.city; // フォームから送信された都市名を取得
    const url = `http://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&lang=ja&units=metric`;

    axios.get(url)
        .then(response => {
        const weatherData = {
            city: response.data.name,
            weather: response.data.weather[0].description,
            temp: response.data.main.temp,
            windspeed: response.data.wind.speed
        };
        res.render('weather', { weather: weatherData }); // 取得した天気情報をweather.ejsで表示
        })
        .catch(error => {
        console.error("エラーが発生しました。", error);
        res.render('error', { errorMessage: '天気情報を取得できませんでした。' });
        });
    });
    ```
    - **app.post('/weather', (req, res))** : /weatherパスに対するPOSTリクエストを処理します。今回はフォームから送信されたデータを元に天気情報を取得するためのエンドポイントになります。  

    - **const city = req.body.city;** : フォームから送信された都市名を取得します。req.body.cityはPOSTリクエストのbodyからcityフィールドの値を取得します。  

    - **const url = ...** : OpenWeatherMap APIへのリクエストURLを組み立てています。cityとapiKeyをURLパラメータとして、指定された都市の天気情報を取得します。  

    - **axios.get(url)** : axiosを使ってAPIリクエストを送信しています。  

    - **then(response => { ... })** : リクエストが成功した場合にAPIから返されたレスポンスを処理します。response.dataにはAPIのレスポンスデータが含まれています。
        - **weatherData** : 今回取得したい気象情報（都市名、天気の説明、気温、風速）をオブジェクトとして格納しています。
        - **res.render('weather', { weather: weatherData })** : 取得した天気情報をweather.ejsテンプレートに渡して表示します。  

    - **catch(error => { ... })** : リクエストが失敗した場合のエラーハンドリングです。エラーが発生した場合、エラーメッセージを表示するerror.ejsをレンダリングします。  

8. **サーバーの起動**
    ```javascript
    app.listen(port, () => {
    console.log(`サーバーがポート${port}で起動しました`);
    });
    ```
    - **app.listen(port)** : Expressサーバーを指定したポート番号で起動します。今回のケースでは **http:localhost:8080** でのアクセスが可能です。

    - **console.log(...)** : 分かりやすいようにここではサーバーが起動したことをコンソールに表示しています。

### ② ejs（３種類ともやってることは同じなのでここではweather.ejsを取り上げます）
  作成したejsですが、下記のコードで動的なデータの埋め込みを行っています。
  ```html
    <h1><%= weather.city %>の天気情報</h1>
    <p>天気: <%= weather.weather %></p>
    <p>温度: <%= weather.temp %>°C</p>
    <p>風速: <%= weather.windspeed %>m/s</p>
  ```
  EJSテンプレートでは、**<%= %>** の構文を使って、サーバーから渡されたデータをHTMLに埋め込みます。  
  今回の例で言うと、weatherというオブジェクトから、city、weather、temp、windspeedというプロパティを取得して、HTMLに埋め込んでいる、という形です。
  
## 出力結果
http:localhost:8080にアクセスすると、下記が表示されます。  

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715906/2bc1d08b-0e1b-5055-447c-06505f726215.png)


都市名はTokyoというようにアルファベットで入力します。
検索すると、

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715906/7b89fd3f-2334-1bfe-f799-b6a90cd98c73.png)


このように東京都の天気情報が取得できました。

## おまけ

  今回は「天気」「温度」「風速」の3種類のデータを取得しましたが、別のデータを表示させることも可能です。今回の結果で取得できたAPIレスポンスボディは下記になります。

  ```json
  {
  "coord": {
    "lon": 139.6917,
    "lat": 35.6895
  },
  "weather": [
    {
      "id": 803,
      "main": "Clouds",
      "description": "曇りがち",
      "icon": "04n"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 24.11,
    "feels_like": 24.35,
    "temp_min": 23.47,
    "temp_max": 24.79,
    "pressure": 1016,
    "humidity": 68,
    "sea_level": 1016,
    "grnd_level": 1014
  },
  "visibility": 10000,
  "wind": {
    "speed": 6.69,
    "deg": 50
  },
  "clouds": {
    "all": 75
  },
  "dt": 1727686200,
  "sys": {
    "type": 2,
    "id": 268395,
    "country": "JP",
    "sunrise": 1727642111,
    "sunset": 1727684831
  },
  "timezone": 32400,
  "id": 1850144,
  "name": "東京都",
  "cod": 200
}
  ```
  例えば湿度も取得したいという場合であれば、**humidity** という項目を取得してあげれば表示が可能です。
  app.js の変更部分のみ下記に記載します。

```javascript:app.js
  axios.get(url)
    .then(response => {
      const weatherData = {
        city: response.data.name,
        weather: response.data.weather[0].description,
        temp: response.data.main.temp,
        windspeed: response.data.wind.speed,
        humidity: response.data.main.humidity
      };
      res.render('weather', { weather: weatherData }); // 取得した天気情報をweather.ejsで表示
    })
    .catch(error => {
      console.error("エラーが発生しました。", error);
      res.render('error', { errorMessage: '天気情報を取得できませんでした。' });
    });
```

そして、weather.ejs も一部変更してあげます。  

```html:weather.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天気情報</title>
</head>
<body>
    <h1><%= weather.city %>の天気情報</h1>
    <p>天気: <%= weather.weather %></p>
    <p>温度: <%= weather.temp %>°C</p>
    <p>風速: <%= weather.windspeed %>m/s</p>
    <p>湿度: <%= weather.humidity %>%</p>
    <a href="/">戻る</a>
</body>
</html>
```

下記が実行結果です。
問題なく取得できました。  

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3715906/563806ba-03ef-5a23-728b-55da0b2d239a.png)


## まとめ
  Node.jsやExpressのキャッチアップと併せて、Web APIの仕組みも理解できてよかったです。  
  フロントやAPIの理解が深まると作成するWEBアプリケーションの幅も随分広がると思うので、バックエンドの学習と併せて学習を続けていきたいです。