grift-sense

呼吸のしやすいように生きていたいだけ

Firebase x TypeScript x Express で爆速でAPIを作ってDB(Firestore)にデータを保存する

やりたいこと

新人のReact研修用にアプリをつくってもらうのに、APIサーバとDBが必要になったので Firebase をつかって実装することにした。 別の案件もあってほとんど時間が取れないので爆速で実装する必要がある。

実装する

1. Firebase でプロジェクトを作成する

2. Functions の準備

3. Firestore の準備

4. ローカルに作成したプロジェクトと Firebase を連携させる

プロジェクト用のディレクトリを作成。

// package.json 作成
npm init
// firebase を扱うツールをインストール
npm i -D firebase-tools
firebase init

[操作] 矢印カーソルで移動、スペースで選択 Firestore と Functions を選択して Enter

? Which Firebase CLI features do you want to set up for this folder? Press Space to 
select features, then Enter to confirm your choices. 
 ◯ Database: Configure Firebase Realtime Database and deploy rules
 ◉ Firestore: Deploy rules and create indexes for Firestore
❯◉ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features
 ◯ Remote Config: Get, deploy, and rollback configurations for Remote Config

Use an existing project を選択して Enter

? Please select an option: (Use arrow keys)
❯ Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project

Firebase で作成したプロジェクトを選択して Enter

? Select a default Firebase project for this directory:
  hoge-app (hoge)
  hoge-app2 (hoge)
  hoge-app3 (hoge)
❯ xxxx-xxxxxxxxxx-app (xxxxx-xxxxxxxxx-app)
  hoge-app4 (hgoe)
  hoge-app5 (hgoe)
  hoge-app6 (hoge)

そのままEnter

=== Firestore Setup

Firestore Security Rules allow you to define how and when to allow
requests. You can keep these rules in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore Rules? (firestore.rules) 

そのままEnter

Firestore indexes allow you to perform complex queries while
maintaining performance that scales with the size of the result
set. You can keep index definitions in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore indexes? firestore.indexes.json

TypeScript を選んでEnter

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? (Use arrow keys)
  JavaScript 
❯ TypeScript 

そのまま Enter

? Do you want to use ESLint to catch probable bugs and enforce style? (Y/n)

そのまま Enter

✔  Wrote functions/package.json
✔  Wrote functions/.eslintrc.js
✔  Wrote functions/tsconfig.json
✔  Wrote functions/tsconfig.dev.json
✔  Wrote functions/src/index.ts
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? (Y/n) 

Firebase プロジェクトの初期化完了。

5. とりあえずデプロイしてAPIをテストする (エラーが出る)

functions/index.ts の一部のコメントアウトを外す

import * as functions from "firebase-functions";

// // Start writing Firebase Functions
// // https://firebase.google.com/docs/functions/typescript
//
// ここのコメントアウトを外す
export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

デプロイ(エラー出る)

# デプロイ(エラー出る)
$ firebase deploy --only functions

エラー内容

/Users/yanagi/dev/blog/test-functions/functions/.eslintrc.js
  13:44  error  Missing trailing comma  comma-dangle

functions/.eslintrc.js の13行目にカンマがないよと言われているので、カンマをつける。

カンマをつけたらもう一度デプロイ

# デプロイ
$ firebase deploy --only functions

今度はうまくいく。 デプロイが完了したらコンソールからAPIを叩いてみる。Hello from Firebase!が返ってくればOK。

URLはFirebaseのプロジェクトのFunctionsにで見れる。(他にも確認のしかたあるかもしれん)

# リクエスト
$ curl https://hgoehgoe.cloudfunctions.net/helloWorld
Hello from Firebase!

6. Express でAPIを作る

express で実装するAPIと functions.https.onRequest で実装されているいまのAPIを削除するAPIが混在するとなぜかcorsエラーが消えないという呪いにかかったので、とりあえず最初から書かれている functions.https.onRequest で実装されているAPIを削除しておく。

// functions/index.ts
import * as functions from "firebase-functions";

// // Start writing Firebase Functions
// // https://firebase.google.com/docs/functions/typescript
//
// ここ以下を削除しておく
export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

express を install

$ cd functions
$ npm i express
$ npm i -D @types/express

APIを実装

import * as functions from "firebase-functions";
import * as express from "express";

const app = express();

app.get("/hello", (req, res) => {
  res.send("hello express API");
});

const api = functions.https.onRequest(app);
module.exports = {api};

デプロイしてテスト (hello express API が返ってくればOK)

# リクエスト
$ curl curl https://hgoehgoe.cloudfunctions.net/api/hello
hello express API

とりあえずAPIは完成。

7. cors対応をしておく

基本的にブラウザからAPIを叩くことになると思うのでcors対応をしておく

現状のままブラウザからAPIを叩くと、下記のようなcorsエラーが返ってくる

Access to fetch at 'https://hogehoge.cloudfunctions.net/api/hello' from origin 'https://console.firebase.google.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

この cors エラーを回避する処理を実装する

cors をインストール

$ npm i cors

corsエラー回避処理を実装

import * as functions from "firebase-functions";
import * as express from "express";
import * as cors from "cors";

const app = express();
// cors対策
app.use(cors({origin: true}));

app.get("/hello", (req, res) => {
  res.send("hello express API");
});

const api = functions.https.onRequest(app);
module.exports = {api};

これでブラウザからAPIを叩いてもcorsエラーが起きなくなる。

8. Firestore をつかってCRUDを実装する

firebase-admin をインストール

$ npm i firebase-admin

まずはデータがなくては始まらないので、Firestoreにデータを登録できるようにする。

今回は Firebase の members というコレクションにデータを登録することにする。

import * as functions from "firebase-functions";
import * as express from "express";
import * as cors from "cors";
import * as admin from "firebase-admin";

const app = express();
// cors対策
app.use(cors({origin: true}));
// リクエストデータ取得のためのパーサ登録
app.use(express.json());
// firebase-admin初期化
admin.initializeApp();

app.post("/members", async (req, res) => {
  type RequestData = {
    name: string,
    message: string,
  };
  // リクエストのデータを取得
  const requestData: RequestData = req.body;
  // Firestoreのmembersコレクションにデータを保存
  await admin
    .firestore()
    .collection("members")
    .doc()
    .set(requestData);
  res.send({message: "success"});
});

const api = functions.https.onRequest(app);
module.exports = {api};

追加部分を説明していく

リクエストで送られてくるjsonデータをオブジェトにパースしてくれるパーサを登録している

// リクエストデータ取得のためのパーサ登録
app.use(express.json());

firebase-adminの初期化処理。これをやっておかないと Firestore に接続できない。

// firebase-admin初期化
admin.initializeApp();

リクエストされたデータの取得。 パーサを登録しているので、オブジェクトで取得できる。

// リクエストのデータを取得
const requestData: RequestData = req.body;

メインの Firestore にデータを登録する処理。 めちゃめちゃかんたんに書けるので Firestore 大好きである。

// Firestoreのmembersコレクションにデータを保存
  await admin
    .firestore()
    .collection("members")
    .doc()
    .set(requestData);
  res.send({message: "success"});

エラーのハンドリング処理は入れていないので、自分で入れて下さい!

デプロイしてテスト ({"message":"success"}がかえってくればOK)

# リクエスト
$ curl -X POST -H "Content-Type: application/json" -d '{"name":"ディオ", "message":"おまえは今まで食ったパンの枚数をおぼえ  ているのか?"}' https://hogehoge.cloudfunctions.net/api/members

# {"message":"success"}がかえってくればOK
{"message":"success"}

{"message":"success"}が返ってきたら Firebase のコンソールから Firestore のコンソールを開いてデータを確認する。 ちゃんとデータがはいっていればOK。

9. Firebase からデータを読み取る処理を書く

import * as functions from "firebase-functions";
import * as express from "express";
import * as cors from "cors";
import * as admin from "firebase-admin";

const app = express();
// cors対策
app.use(cors({origin: true}));
// リクエストデータ取得のためのパーサ登録
app.use(express.json());
// firebase-admin初期化
admin.initializeApp();

// members取得API
app.get("/members", async (req, res) => {
  // Firebaseからデータ取得
  const members = await admin
    .firestore()
    .collection("members")
    .get()
    .then(async (snapshot) => await snapshot.docs.map((v) => v.data()));
  res.send({members});
});

// members登録API
app.post("/members", async (req, res) => {
  type RequestData = {
    name: string,
    message: string,
  };
  // リクエストのデータを取得
  const requestData: RequestData = req.body;
  // Firestoreのmembersコレクションにデータを保存
  await admin
    .firestore()
    .collection("members")
    .doc()
    .set(requestData);
  res.send({message: "success"});
});

const api = functions.https.onRequest(app);
module.exports = {api};

追加部分の説明

membersデータ取得のAPI。Firestore からデータ取得する処理書いただけ。

// members取得API
app.get("/members", async (req, res) => {
  // Firebaseからデータ取得
  const members = await admin
    .firestore()
    .collection("members")
    .get()
    .then(async (snapshot) => await snapshot.docs.map((v) => v.data()));
  res.send({members});
});

デプロイしてテスト (さっき登録したデータが返ってきたらOK)

# リクエスト
curl https://hgoehgoe.cloudfunctions.net/api/members

# さっき登録したデータが返ってきたらOK
{"members":[{"message":"おまえは今まで食ったパンの枚数をおぼえているのか?","name":"ディオ"}]}     

PUT と DELETE はつかれたからまた今度

おまけ

[VSCode使ってるひとだけ] functions/index.ts の一行目の import で ESLint のエラーが出てると思うのでそれを解消する

この記事を読んでくれ。

flow-flow-flow.hatenablog.com

おわり