Netlify Functions を使って CORS エラーを回避する

## はじめに

現在 Web アプリを Nuxt で開発していて、`nuxt generate`で静的コンテンツを作成し、Netlify にホスティングしている
そのアプリでは、別ドメインの API にリクエストを投げてデータを取得しているので、CORS ポリシーでブロックされないように[proxy-module](https://github.com/nuxt-community/proxy-module)を使って API と同一ドメインになるようにプロキシしている

```js:nuxt.config.js
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
proxy: {
  '/atnd': {
    target: 'http://api.atnd.org/events',
    pathRewrite: {
      '^/atnd': '/'
    }
  },
  '/connpass': {
    target: 'https://connpass.com/api/v1/event',
    pathRewrite: {
      '^/connpass': '/'
    }
  }
}
```

ただし、上記の proxy モジュールによる書き換えはサーバーが必要で、generate 機能を使って静的コンテンツをホスティングしている場合は機能しない
ちなみにローカルでは webpack-dev-server などを使って、Node サーバー上で動かしていれば、ちゃんとプロキシされる

この辺のことは以下に詳しく書いてある
https://github.com/nuxt-community/proxy-module/issues/1

Nuxt の generate 機能を使っていても、ちゃんと別ドメインの API が叩けるようにするというのが今回の開発の目的です

作業リポジトリはこちら
https://github.com/kurosame/event-search

## Netlify Functions を使う

ブラウザには別ドメインへの通信を拒否する仕組みが標準で備わっている
Nuxt のプロキシ機能はサーバーが無いと使えないし、API 側にオリジンを許可させることもできない

これらのことを踏まえて、今回は Netlify Functions を使って Lambda 関数内で API リクエストを実行することで CORS エラーを回避することにした

Netlify Functions の実行環境は AWS Lambda だが、AWS 側の設定は一切やらなくて良い
てか AWS アカウントも不要

Netlify のアカウントがあれば使えるが、フリープランだと以下の制限がある

- 125000 リクエスト/月
- 100 時間/月

言語は JS と Go をサポートしている
（本記事では JS で実装している）

## ローカルで動作確認

以下のモジュールをインストール

```
yarn add -D netlify-lambda
```

設定ファイルを用意する

```toml:netlify.toml
[build]
  functions = "functions/dist"
```

Lambda にやらせたい処理を書いて、handler 関数を export

```ts:functions/src/sample.ts
export async function handler(event, context) {
  return {
    statusCode: 200,
    body: 'Hello, World'
  }
}
```

```json:package.json
"scripts": {
  "lambda": "netlify-lambda serve functions/src"
},
```

```
yarn lambda
```

以下のようにリクエストする
`http://localhost:9000/sample`

## Functions から connpassAPI を叩いてみる

以下のコードが Functions 関数の実装
API の仕様上、100 件以上レスポンスが存在していても、MAX100 件しかデータが取れないので再帰関数にしているが、やっていることは API から取ってきたデータを body に JSON.stringify にして渡しているだけです
IConnpassEventResponse と IConnpassResponse の型インターフェースは後述してます

```
yarn add -D @types/aws-lambda
```

```ts:functions/src/connpass.ts
import { IConnpassEventResponse, IConnpassResponse } from '@/store/connpass'
import axios, { AxiosResponse } from 'axios'
import { APIGatewayProxyEvent } from 'aws-lambda'

export async function handler(event: APIGatewayProxyEvent) {
  const ymd = (event.queryStringParameters || { period: '' }).period
  const count = 100
  const getEvents = (
    events: IConnpassEventResponse[] = [],
    start: number = 1
  ): Promise<IConnpassEventResponse[]> =>
    axios
      .get('https://connpass.com/api/v1/event', {
        params: { ymd, start, count }
      })
      .then((res: AxiosResponse<IConnpassResponse>) =>
        res.data.results_returned === count
          ? getEvents([...events, ...res.data.events], start + count)
          : [...events, ...res.data.events]
      )

  const events: IConnpassEventResponse[] = await getEvents()

  return {
    statusCode: 200,
    body: JSON.stringify(events)
  }
}
```

TS で書いた場合は、以下を functions ディレクトリに追加

```
yarn add -D @babel/preset-typescript
```

```ts:functions/.babelrc
{
  "presets": ["@babel/preset-typescript", "@babel/preset-env"],
  "plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]]
}
```

基本的に presets だけでコンパイルエラーは回避できると思うが、plugins はエラーになったらそのエラー名で検索するとたぶん必要なプラグインが分かると思うので、必要に応じて設定する
作った後に気づいたけど、[ここ](https://github.com/netlify/netlify-lambda#use-with-typescript)に書いてた

```
yarn lambda
```

正常に起動したら、以下にアクセス
`http://localhost:9000/connpass`

たぶん Lambda 側でタイムアウトする（デフォルト 10 秒）ので、日付で絞るかタイムアウト時間を伸ばすかしてみてください
`http://localhost:9000/connpass?period=20190624`
or
`yarn lambda -t 20`

## Nuxt から Functions にリクエストする

Vuex のアクションで axios を使って Functions にリクエストしている

`?period=${period}`の書き方について
⇒ `axios.get`の param オプションを使うと、Functions 側の event 引数に入らなかったので、直接 URL にパラメータを書いている

```ts:store/connpass.ts
import { IEventState } from '@/store/events'

export interface IConnpassEventResponse {
  title: string
  catch: string
  description: string
  event_url: string
  started_at: string
  ended_at: string
  limit: number
  address: string
  place: string
}

export interface IConnpassResponse {
  results_returned: number
  events: IConnpassEventResponse[]
}

export const actions = {
  async getConnpassEvents({ commit }, period: string) {
    const events: IConnpassEventResponse[] = await (this as any).$axios.$get(
      `/connpass?period=${period}`
    )

    // Functionsのレスポンスを色々加工してStoreに入れている
    // （ここから以下は本記事の目的とは関係ない部分）
    commit(
      'events/setEvents',
      events
        .filter((e: IConnpassEventResponse) => e.limit >= 30)
        .map(
          (e: IConnpassEventResponse) =>
            ({
              title: e.title,
              catch: e.catch,
              description: e.description,
              eventUrl: e.event_url,
              startedAt: (this as any)
                .$moment(e.started_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              endedAt: (this as any)
                .$moment(e.ended_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              address: `${e.address} ${e.place}`
            } as IEventState)
        ),
      { root: true }
    )
  }
}
```

ローカルで netlify-lambda を使って起動するとポート 9000 で実行される
Nuxt はポート 3000 で起動しているので、以下のようにプロキシしないと CORS に引っかかる

Netlify にホスティングして動かす場合は、Nuxt と Functions は同じドメイン上で動くので、プロキシは不要
（そうじゃないと今回 Functions を使った意味がないので、、）

```js:nuxt.config.js
proxy: {
  '/connpass': {
    target: 'http://localhost:9000'
  }
}
```

最後にアクションを Dispatch する

```js:components/Summary.vue
this.$store.dispatch('connpass/getConnpassEvents', '20190624,20190625')
```

動作確認
ローカルで Lambda を起動して、Nuxt でアクションを Dispatch する

```
yarn lambda -t 100
```

## ホスティングして動作確認

netlify.toml がルートディレクトリに存在する場合、Netlify の管理画面上の設定は無視される
その場合、netlify.toml にデプロイ設定を書く必要がある

```toml:netlify.toml
[build]
  command = "yarn generate"
  functions = "functions/dist"
  publish = "dist"
```

Netlify にホスティングすると、Functions は`https://[Site name].netlify.com/.netlify/functions`というパスになるので、axios の baseURL を Functions の URL にする

```js:nuxt.config.js
axios: { baseURL: '/.netlify/functions' },
proxy: {
  '/.netlify/functions/connpass': {
    target: 'http://localhost:9000'
  }
}
```

ちなみに netlify-lambda を使ってローカルでビルドした場合は`http://localhost:9000`と`http://localhost:9000/.netlify/functions`のどちらでも良いので、上記の proxy 設定でアクセスできる

ただし、axios の baseURL を Functions に固定するのは後々不便になるかもしれない
その場合は、環境変数を使って対応した方がいいかも

後は Netlify 上で GitHub リポジトリを紐づけて、その GitHub リポジトリのブランチに push すればホスティングされて動作するはずです