Yuki's Tech Blog

仕事で得た知見や勉強した技術を書きます。

【Web API】CRUD機能のエンドポイントとリクエスト、レスポンスをどのように設計するかをざっくりまとめてみた

目次

概要

最近APIを実装することが多かったので、忘れないようにブログにまとめます。

APIとは

APIとは、外部のプログラムからあるプログラムの機能を呼び出すための手続きを定めた規約のことです。つまり、決まりごとに過ぎないのです。APIに従ってあるプログラムの機能を呼び出す短いコードをプログラムに追加することによって、プログラム上でその機能を利用することができます。APIの詳細についてはAPIドキュメントとして公開されています。そのAPIドキュメントを見ることによって、どんな機能が提供されているのか、どんな手順でこの機能を利用できるのかが分かります。

余談ですが、「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典のAPIの説明が分かりやすかったので以下に引用します。

本当は外部のプログラムとかからその機能にお仕事を依頼するための窓口の使い方とかに関する決まり事がAPIです。
例えば「お金を入れるとケーキを作る機能」のAPIであれば
・「お金を入れるとケーキを作る機能」にお仕事を依頼するための窓口は、どこにあるの?
・窓口を使うときは無言でお金を渡せばいいの?何か一声かける必要がある?
・窓口に渡すお金は現金?小切手?カード払いはいける?
・窓口から出てきたケーキは、どうやって受け取ればいいの?お皿を用意しておく必要がある?
などの使い方ルールを含みます。
ただし、一般的には、窓口そのものと捉えた方がイメージしやすいはずです。

API (エーピーアイ)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

APIと実装

APIはあくまで「外部のプログラムからあるプログラムの機能を呼び出すための手続きを定めた規約」です。つまり、決まりごとに過ぎないです。呼び出される機能を実装したプログラムが存在して、初めてAPIに挙げられた機能を利用できます。

Web APIとは

Web APIとは、HTTPプロトコルを利用してネットワーク越しにプログラムを呼び出すための手続きを定めた規約のことです。ざっくり言うとWebベースのAPIです。

Web APIにおけるエンドポイントとは

Web APIにおけるエンドポイントとは、APIにアクセスするためのURIのことです。APIは通常様々な機能がセットになっているので、複数のエンドポイントを持ちます。

リソースとは

リソースとは、何らかのデータのことであり、APIであればそのエンドポイントで取得できる情報のことです。つまり、エンドポイントは、HTTPメソッドで操作する対象( = リソース)を表していると分かります。

Web APIのエンドポイント、リクエスト、レスポンス

例として、「あるユーザーに紐づくタスクに対してのCRUD機能」をAPIで提供するケースを考えます。ドメインsample.xyzとします。

余談ですが、先ほどエンドポイントはリソースを表すと言いましたが、エンドポイントの複数形の部分は、あるデータの集合を表します。そして、エンドポイントのidなどは、個々のデータを表しています。 この決まりさえ分かれば、http://sample.xyz/api/usersにGETリクエストを出した場合、ユーザー一覧が取得できることも感覚的に理解できます。

GET

GETメソッドはリソースを取得する際に使用します。

タスク一覧機能

■ エンドポイント
http://sample.xyz/api/users/{id}/tasks

■リクエス
- リクエストボディはなし

■レスポンス
- ステータスコードは200 OK
- レスポンスボディにユーザーに紐づくタスク一覧が格納されている
- リクエスト失敗した場合、ステータスコード404 Not Found
- 404 Not Foundは指定したリソースが見つからないことを表す

タスク詳細機能

■ エンドポイント
http://sample.xyz/api/users/{id}/tasks/{task_id}

■リクエス
- リクエストボディはなし

■レスポンス
- ステータスコードは200 OK
- 200 OKは、リクエストが成功したことを表す
- レスポンスボディにユーザーに紐づく指定したタスクが格納されている
- リクエスト失敗した場合、ステータスコード404 Not Found

POST

POSTメソッドは、リソースを新規登録する際に使用します。POSTリクエストは、データの集合を表すエンドポイントに対して行います。成功すると、新しいデータがデータの集合配下に作成されます。そして、その新しいデータを表すエンドポイントでそのデータを取得できるようになります。

タスク登録機能

■エンドポイント
http://sample.xyz/api/users/{id}/tasks

■リクエス
- リクエストボディにサーバー側に送るタスク情報が格納されている

■レスポンス
- ステータスコードは201 Created
- 201 Createdは、リクエストが成功し、新しいリソースが作成されたことを表す
- レスポンスボディには新規作成したタスクが格納されている。レスポンスボディに新規作成したタスクを格納することで、タスク登録機能を実行した後にフロント側でタスク詳細のAPIを叩かなくて済む
- リクエスト失敗した場合、ステータスコードは400 Bad Request
- 400 Bad Requestはリクエストが正しくないことを表す

PATCH

PATCHメソッドは、指定したリソースの一部分を更新する際に使用します。PUTメソッドは送信するデータで元々のリソースを完全に上書きしてしまうらしいです(未検証)。指定したリソースを更新したい場合はPATCHメソッドを使用しましょう。

タスク更新機能

■エンドポイント
http://sample.xyz/api/users/{id}/tasks/{task_id}

■リクエス
- リクエストボディには、更新するデータが格納されている

■レスポンス
- ステータスコードは200 OK
- レスポンスボディには更新したデータが格納されている
- リクエスト失敗した場合、ステータスコードは400 Bad Requestまたは404 Not Found

DELETE

DELETEメソッドは、指定したリソースの削除をする際に使用します。エンドポイントで指定したリソースを削除します。

タスク削除機能

■エンドポイント
http://sample.xyz/api/users/{id}/tasks/{task_id}

■リクエス
- リクエストボディはなし

■レスポンス - ステータスコードは204 No Content
- 204 No Contentは、リクエストが成功したがレスポンスボディが空であることを表します。
- 削除した後に、もう一度削除したリソースを使うというケースはあまり考えられないため、204 No Contentにしています。
- リクエスト失敗した場合、ステータスコード404 Not Found

まとめ

「あるユーザーに紐づくタスクに対してのCRUD機能」をAPIで提供した場合、5つ機能があるのに必要なエンドポイントは2つしかないことが分かりました。エンジニアになりたての頃、5つ機能があるから5つエンドポイントが必要なのかと勘違いしていたのですが、そんなことはないのでAPI設計するときは注意しましょう。

参考

Web API: The Good Parts | 水野 貴明 |本 | 通販 | Amazon

Rails のルーティング - Railsガイド

API(アプリケーションプログラミングインターフェース)とは - 意味をわかりやすく - IT用語辞典 e-Words

API の種類|Sheeprograming

RESTful API設計におけるHTTPステータスコードの指針 - Qiita

アプリケーションプログラミングインタフェース - Wikipedia

Web APIとは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

API (エーピーアイ)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

【2022/12/21 ~ 2023/1/7】コードレビューで知ったことをざっくりまとめてみた

目次

概要

最近コードレビューを受けて知ったことをまとめます。

Open API

どんなリソースを操作しているかが分かるパスを書く

APIのパスは短ければ良いのかと思ってました。しかし、そういうわけではなく、どんなリソースを操作しているのかがひと目見たら分かるようなパスが良いそうです。

使い手の意図しない挙動が発生するようなAPIを作らない

APIの使い手にとって、挙動が想像しやすいAPIほど使いやすいです。APIの使い手が意図しない挙動を実装するのは使いずらくなるので、やめましょう。

Rails

has_manyで指定する関連モデル名は複数形にする

has_manyで指定する関連モデル名は複数形にする必要があるので、忘れないようにしましょう。

Active Recordオブジェクトの保存時に何か処理を実行したい場合、モデルにコールバックを書く

RailsにおけるコールバックはRailsドキュメントに詳細が書いてあったので引用させていただきます。

コールバックとは、オブジェクトのライフサイクル期間における特定の瞬間に呼び出されるメソッドのことです。コールバックを利用することで、Active Recordオブジェクトが作成/保存/更新/削除/検証/データベースからの読み込み、などのイベント発生時に常に実行されるコードを書くことができます。

つまり、モデルにコールバックを書いておくことで、オブジェクトライフサイクルにおける特定のイベント発生時に、指定したメソッドを呼び出すことができます。 以下の例では、ブロックをafter_createコールバックとして呼ぶということをProductモデルに定義しています。

class Product < ApplicationRecord
  # 省略
  after_create do
    product.update!(surveyed_at: Time.current)
  end
end

hashid-railsのhashidをfindに渡しても検索できる

hashid-railsのhashidをfindに渡しても検索できます。そのため、find_by_hashidを使わなくても大丈夫です。

create_paramsとupdate_paramsが共通とは限らないので、どちらも定義する

将来的に保持する値が異なる可能性があるので、現状が同じだとしても、別々に定義しましょう。

Rspec

メッセージの検証は不要

場合によりますが、ステータスコードだけ分かれば良い場合、メッセージの検証は不要です。

単体取得のAPIをテストする場合、create_listで複数データを作る必要はない

Specは積み重なると、全部実行するのにそこそこ時間がかかります。そのため、節約できるところは節約しましょう。今回の場合は、単体取得のAPIをテストしたいだけなので、create_listを使わずに普通にcreateで単体データを作成すれば大丈夫です。

テストコード上で無駄に具体的な値を指定しない

テストコード上では「何か具体的な値を指定をする場合、意味があるものだ」という認識で見ます。そのため、もし意味がないなら指定しないようにしましょう。

let, let!, beforeをいつ使うのかを明文化する

beforeで作成するデータと、letで直接作成するデータの違いが、よく読み解かないと分からないため、明文化します

  • なるべく let を用いて定義する
  • let! を使わないとどうしようとないものに関してのみ let! を使う
  • 込み入った前提データの作成があれば before で行う

React

propsでFactory関数を受け取らない

この状況の場合、Factory関数は子コンポーネントのonClickイベントにアローを使わずに関数を指定したいから作っています。つまり、子コンポーネントの都合なので、親コンポーネントでFactory関数を定義するのではなく、子コンポーネントに定義しましょう。

TSでクラスのインスタンスを作るときに、括弧を省略しない

TSでクラスのインスタンスを作るときに括弧を省略できます。しかし、分かりづらいのでしないようにしましょう。

■before

new Product

■after

new Product()

モデルのインスタンスの値をハードコーディングしている場合、疑似enumを使う

ハードコーディングを管理していくのは辛いので、擬似enumを利用して、定数を一元管理しましょう。この擬似enumはモデルが持つべき責任なので、モデルのファイルに書いておきましょう。

// 擬似enum
const StorageTemperatures = {
  Normal: "normal",
  Frozen: "frozen",
} as const;
type StorageTemperatures = typeof StorageTemperatures[keyof typeof StorageTemperatures];

フォーマットを変換する系の関数の命名は、convertよりformatの方が良い

format~の方が意味がわかりやすいので、良いです。

モデルのインスタンスを生成しないとできない処理で、インスタンスを生成する必要がないと判断した場合、ヘルパー関数で定義する

モデルのインスタンスを生成しないとできないような処理は、モデルのインスタンスを事前に作る必要があるので面倒です。format系の関数はヘルパー関数として定義した方が使い勝手が良いです。

各modelクラスのメンバーの型を定義する場合、モデル名 + メンバーの型名にする

各modelクラスのメンバーの型を制限する型は、 モデル名 + メンバーのようにモデル名のprefixがついている方が良いです。 これはメンバーの名称の幅が広すぎる場合、名前が衝突する可能性があるためです。

バランスを考えてDRYにする

ベタ書きしすぎても辛いですし、過度にDRYにしすぎても定義元を見たりロジックを読み解かないといけなかったりするので、バランスを考えてDRYにした方が良いです。メリットが多い場合にDRYにした方が良いです。

テーブルヘッダーのカラムにはthタグを使う

テーブルヘッダーのカラムにはthタグを使うので、忘れないようにしましょう。

index.tsxとindex.tsを同階層に配置しない

パスを書くときにindex.tsxが反応しなかったりするので、やめましょう。

TSは関数もオブジェクトなので、メンバーを持つ関数を定義することができる

TSの関数はオブジェクトなので、メンバーを持つ関数を定義できます。ちなみにメンバーとは、クラスやオブジェクト(インスタンス)の持つ変数や関数などのことです。オブジェクトの持つ変数をメンバ変数、オブジェクトの持つ関数をメンバ関数と呼んだりします。言語によってはメンバ変数をプロパティ、メンバ関数をメソッドと呼んだりします。

以下のようにして、関数にメンバーを持たせることができます。

type SomeFuncWithMembers = (() => void) & {
  a: string;
  b: number;
};

const some: SomeFuncWithMembers = () => void(0);
some.a = "aaa";
some.b = 1;

console.log(some.a); // => "aaa" 
console.log(some.b); // => 1

Reactコンポーネントは関数なので、コンポーネントをメンバーに持つコンポーネントを定義することができます。こうすることで、名前空間の衝突を防ぐことができます。テーブルコンポーネントなどの割と名前が衝突しやすいコンポーネントで行うと良いです。

zodで文字列を必須にする場合は、min(1)をつければ良い

zodで文字列を必須にする場合は、min(1)をつけましょう。

更新系の関数もuseProductで持つ

セットにすることでupdate後のstateの更新がやりやすいです、また、更新だけを単独で走らせたいケースはあんまりなく、大体get処理とセットになるためです。そのため、更新系の関数を返すuseUpdateProductを定義する必要はないです。更新系の関数もuseProductで返すようにしましょう。

Props は 慣習上コンポーネントの引数に対して用いる名称

Props は 慣習上コンポーネントの引数に対して用いる名称です。関数の引数の型などに、むやみにPropsと命名しないようにしましょう。

同じpathからのインポートの場合、まとめる

同じpathからのインポートの場合、まとめるようにしましょう。

無駄に条件を適用させる範囲を広げない

フォームコンポーネントの表示をある条件で制御したいのに、フォームコンポーネントの外側のJSXも含めて非表示にする必要はないです。指定した条件で、くくる範囲を広くしすぎないようにしましょう。

Next.jsの環境変数で真偽値を持ちたい場合、0か1を使う

Next.jsの環境変数は文字列しか使えません。そのため、基本的には0か1を使うようにしましょう。

型にはめ込んだ憶え方をしない

三項演算子を使うのか、ifを使うのか、switchを使うのか、論理和演算子を使うのかみたいな事は、「ケースバイケース」です。 そのため、「こういう時はこう書く」ではなく、都度「どう書くのが自然なのか」「どう書くのが一番問題が起きづらいのか」ということを考えながらコーディングできると良いです。

LayoutはgetLayoutパターンで適用させる

LayoutはgetLayoutパターンを使うことで、Layoutの状態を保持しつつページコンポーネントに動的にLayoutを適用させることができます。

終わり

やっぱフロントの指摘が多いなと感じました。バックエンドのタスクはスピーディーにできるようになってきたが、フロントはまだ指摘が多いので、フロントもスピーディーにできるようになりたいなと感じました。

参考記事

Active Record の関連付け - Railsガイド

Active Record コールバック - Railsガイド

GitHub - jcypret/hashid-rails: Use Hashids (http://hashids.org/ruby/) in your Rails app ActiveRecord models.

メンバとは - 意味をわかりやすく - IT用語辞典 e-Words

Next.js12.xのLayouts機能

Next.jsのレイアウトパターン

【2022/12/3 ~ 2022/12/20】コードレビューで知ったことをざっくりまとめてみた

目次

概要

フロントエンドのタスクをレビューしていただいた際に、知ったことをまとめます。

Open API

OpenAPI 3.1未満の場合はnullable: trueを追加しないとnullが許容されない

OpenAPI 3.1未満の場合はnullable: trueを追加しないとnullが許容されないため、nullable: trueを追加します、stoplight上でnullの表示を消しても良いのですが、分かりやすさのため、APIドキュメントにもnullを書きます。

Rails

Like句の後ろに半角スペースを入れる

Like句の後ろには半角スペースを入れます。

  products = Product.where("sku LIKE ? ", "#{params[:sku]}%")

procよりラムダを使う

ラムダの方が記述が楽なので、procをどうしても使わないといけない場面でなければ、ラムダを使います。

分類を表すカラム名はkind や categoryのような単語を使う

categoryは使いやすいので、kindをまずは優先して使いましょう。

Rubyではスネークケースを使う

Rubyではスネークケースを使うので、変数定義する時に間違えないように気をつけましょう。

Rspec

存在しない値でリクエストを出したい場合、もしかしたら存在しそうな値を使わない

  let(:id) { Faker::Number.number(digits: 10).to_s }

存在しない値でリクエストを出したい場合、以上のコードだとDBに存在するような値が生成される可能性があります。以下のように絶対に存在しない値(桁数を99桁)にするなどして、テストの信頼性を高めるようにします。

  let(:id) { Faker::Number.number(digits: 99).to_s }

無駄にクエリ発行させない

以下のコードではskuを取得するために、pluckでクエリを一回発行しています。

  before do
    create_list(:product, 3)
  end

  let(:sku) { product.pluck(:sku).sample }

しかし、before doの部分でデータを事前に作っているので、そのデータを利用してskuを取得した方が良いです。

  before do
    @products = create_list(:product, 3)
  end

  let(:sku) { @products[0][:sku]  }

素数を検証したい場合、 have_attributeを利用する

配列の要素数を検証したい場合、have_attributeを利用します。

expect(response.body).to be_json_including(data: {
  products: have_attributes(:count, 0)
})

React

無駄に改行しすぎない

無駄に改行することでファイルの行数が増えてしまうので、1行で行けるなら、1行で書きましょう。

z-50などの使う側の都合で決まる情報をコンポーネント自身に指定しない

コンポーネントの責務は基本的には、自分自身に関することです。z-50などは、コンポーネントを使う側の都合であって、それをコンポーネント自身に指定すると、そのコンポーネントを使う側の都合で決まる情報をコンポーネントが持ってしまい、再利用性が下がります(なんで使う側の都合で決まる情報を子コンポーネントが持たないといけないのか)。そして親コンポーネントの情報を持ってしまっているため、コンポーネント同士が独立していないです。そもそもReactでは複数の機能単位で分割された独立性の高いコンポーネントを組み合わせてソフトウェアを開発していきます。その際、1つのコンポーネントがさまざまな責務を背負っていたり、他のコンポーネントに依存していると、1つの責務を変更するたびに他の責務に影響がないかを細かく確認しないといけなかったり、依存しているせいで再利用性が下がる可能性があります(同じような機能をもう一度作る可能性もあるので)。1回限りしか使わないコンポーネントが増えていく未来しか見えず、同じようなコードが量産される原因になるので、独立したコンポーネントを書くように気をつけましょう。こういう親の都合で決まる値はpropsで子コンポーネントに渡します。そうすることで、親に値の情報を持たせつつ、子コンポーネントに渡すことができます。

複数のプロパティが存在する場合、型定義はインラインで行わない

複数のプロパティが存在する場合、型定義をインラインで行うと、見通しが悪くなります。そのため、型エイリアスで定義します。

■before

  const kind: {
    value: "food" | "dailyNecessities";
    label: string;
    checked: boolean;
  } = {
    // 省略

■after

  type Kind = {
    value: "food" | "dailyNecessities";
    label: string;
    checked: boolean;
  };

  const kind: Kind = {
    // 省略

空白を作りたい場合、cssでマージンを設定するか、flexのgapを利用する

特殊文字の空白を使うと意図が分かりづらいので、cssで指定した方が良いです。

■before

<div>
  {`${product.sku}\u00A0\u00A0${product.name}`}
</div>

flexのgapを使うことで、フレックスアイテム間にスペースを作ることができます。以下の場合、x方向にスペースを作っています。

■after

<div className="flex gap-x-4">
  <div>{sku}</div>
  <div>{name}</div>
</div>

Open API Generatorで生成したenumは特殊なので使用しない

自分で定義したenumを使用しましょう。理由が明確にはわかっていないので、今度遭遇した時に調べてみます。

React Hook Formで同じようなデータは配列で管理する

useFieldArrayを使うことで、配列データの要素の追加や削除、管理がしやすくなります。

細かすぎる粒度でコンポーネントを切らない

上記のようなコンポーネントを切るべき明確な理由を考えずに、細かすぎる粒度でコンポーネントを切らない方が良いです。コンポーネントの粒度が細かすぎると、その分コンポーネントのファイルが増えるので、複数のファイルを行ったり来たりして開発効率が落ちる未来が見えます。デメリットしかなくメリットがないのに、コンポーネントを切るのはやめましょう。

下記の記事でその内容が書かれていて分かりやすいです。

極端なパターンを考えて、技術的な落とし所を探っていく - mizdra's blog

Flagmentだけをレスポンスするのではなくnull なりをreturnする

React コンポーネントで何も返したくない場合、Flagmentではなくnullを返します。Flagmentはそもそも、JSXで最上位に複数の要素を配置できなくて、仕方なく1つの要素を配置していたものを解決するためのものです。Flagmentを使うことで、仕方なく配置していた1つの要素を指定しなくて良くなり、かつ余分なDOMが出力されません。Flagmentもそうですが、本来の目的で使うようにしましょう。

コンポーネント内で定義する関数は全てuseCallbackをつける

useCallbackとは、関数自体をメモ化するフックです。親コンポーネントで関数を定義していて関数を子コンポーネントにpropsとして渡している場合、親コンポーネントが再レンダリングされるたびに関数が再生成されるので、子コンポーネントに違う関数をpropsで渡していると判定されてしまいます。その結果、子コンポーネントが再レンダリングします。関数自体をメモ化することで、関数の再生成を防ぎ、子コンポーネントの再レンダリングを防ぐことができます。

クリックしたデータをDOMから参照せずに、onClickに動的生成した関数を渡して取得する

以下のコードでは、DOMを参照してデータを取得しているので、コードが複雑になりがちです。そして分かりづらいです。

■before

  const onClick = (e: MouseEvent<HTMLLIElement>) => {
    const clieckedSku = e.currentTarget?.firstChild?.firstChild?.textContent;
    // 省略

カリー化を使うことで動的に関数を生成して、その動的関数をonClickに渡すことができます。onClickにデータを渡したFactory関数呼び出しを指定することで、動的生成した関数にデータを渡すことができます。そしてその動的生成された関数自体がreturnされて、onClickにセットされます。

■after

const onClickFactory = useCallback((product: Product) => () => {
  // 本来したい処理
}, []);

// 使う場所
Products.map((product) => <li onClick={onClickFactory(product)}>{product.name}</li>;

カリー化を使わなくてもかけますが、同じ関数を使う場面でも無駄に() =>を書かないといけないかつ、Propsにアロー関数を直接渡すのは推奨されていない(useCallbackを使った関数を事前に定義する)ので、カリー化を使った方が良いでしょう。

const onClick = useCallback((product: Product) => {
  // 本来したい処理
}, []);

// 使う場所
Products.map((product) => <li onClick={() => onClick(product)}>{product.name}</li>;

ちなみにFactoryという名前はFactoryパターンから来ています。関数を生成するという意味でFactoryを使っていると思いますが、Factoryパターンをまだあまりわかっていないので、必要になったら調べます。

import時にindexを指定しない(indexは省略できる)

indexという名前は特殊扱いされていて、components/Todo/indexと書かなくても、components/Todoと書けばOKです。

フォームを作る際はバックエンド側のデータの持ち方になるべく準拠する

バックエンド側でのデータの持ち方をフロントエンド用に整形して扱うのはめんどくさいです。その場合、バックエンド側でのデータの持ち方が不適切な可能性があります。

まとめてエクスポートする場合、index.tsを使う

index.tsに以下のような記述をすることで、コンポーネントをまとめてエクスポートすることができます。

export * from "./Todo";
export * from "./TodoList";

相対パスで短く指定できるなら、そうする

相対パスで短く指定できる場合、そうした方が良いです。パスも短くなり可読性が上がります。

import { DefaultValues } from "./";

zodのバリデーション対象が配列の場合、z.arrayを先に書く

zodでは配列を検証する方法が2つ存在します。

const stringArray = z.array(z.string()); // => string[]

// equivalent
const stringArray = z.string().array(); // => string[]

配列であることを検証する場合、先に書いてある方が、シンプルで分かりやすいです。

{
  productLogs: z.array(z.object({
    ...
  })),
}

tailwindでdisabledを指定する場合、大元の汎用コンポーネントに指定する

スタイルを統一させたいためです。ちなみにtailwindでは以下のようにしてdisabled時のスタイルを当てることができます。

disabled:opacity-60

mutate関数を自前で用意するのではなく、useMutationから分割代入で取得する

react queryが自前で用意しないでいいようにしてくれているので、自前で作る必要はないです。

■before

  const create = useCallback(({
    data,
    onSuccess,
    onError,
  }: {
    data: Data,
    onSuccess: ({ data }: { data: PostProducts201ResponseAllOfData }) => void
    onError: () => void
  }) => {
    mutate.mutate(data, { onSuccess, onError });
    // 省略

■after

const { mutate: create } = useMutation((data: Data) => {
  return api.postProducts(data);
});

いろんなデータ操作系のhooksを同時に利用したい場合はほとんどないので、そういう時にだけ呼び出し側でmutate関数に別名をつければ良い

データ操作系のhooksを定義するとき、mutate関数にcreateProductcreateProductLog ~と名前をつけていました。そのような名前をつけるデメリットは、hooksを使うときにその名前で使わないといけないところです。名前が長いので、書くのが面倒です。1つのコンポーネントでデータ操作系のhooksを同時に呼び出すことはほとんどないので、基本mutatet関数の名前はcreateにしつつ、もし複数のデータ操作系のhooksが必要になった場合、呼び出し側で別名をつければOKです。

typeを使った型はコンポーネントの外側で定義する

コンポーネントの中で型定義すると、エクスポートできません。また、コンポーネント自体が型の情報を持ってしまうので、責務が分離できていない状態です。

onSubmitにはvalidation済みのデータを渡すのが前提

React Hook FormではonSubmitを呼び出す前にバリデーション処理が実行されます。そのため、onSubmit内でバリデーション処理を行うのは不適切です。

valuesAsNumberはControllerだと使用できない

input numberで入力しても、zodでバリデーションをかけると値が文字列として認識されていることが分かります。valuesAsNumberは文字列をnumberに変えるためのオプションです。Controllerだと使用できないので、その場合はregisterを使った方が良いです。

enumで型定義する場合、共用体型を使う

TypeScriptではenumを使うことができます。しかし、enumには昔から問題があるので、型を定義する目的であれば共用体型を使用しましょう。

type Kind = "food" | "dailyNecessities"

ちなみにenumとは、複数の定数を一つにまとめて定義したり管理したりすることができるものです。もしenumを使う場合、以下のように疑似enumを定義しましょう。疑似enumを使うことで定数の一元管理もできて、ハードコーディングを防ぐことができます。

const Kind = {
  Food: "food",
  DailyNecessities: "dailyNecessities",
} as const;

type Kind = typeof Kind[keyof typeof Kind] // => type Kind = "food" | "dailyNecessities"

console.log(Kind.Food); // => food

モデルにimmutableの設定を書く

immerというjsのライブラリを使うことで、ミュータブルなコードをイミュータブルにしてくれるそうです。未検証なので、もし実装する際はやってみようと思います。

export class Product implements Omit<api.Product, "storageTemperature" | "kind"> {
  [immerable] = true;

  // 省略

setStateでprevを使わない場合、書かなくて良い

setStateでprevを使わない場合、書かなくて大丈夫です。

■before

setState((prev) => [])

■after

setState([])

目的に対してかなり複雑な型定義をしない

複雑な型を定義することで、可読性が下がるので、目的に対して本当にこんな複雑な型を定義すべきなのかを考えた方が良いです。

定義元を見に行かないとどんなデータが渡されていたり、検証されているか分からない場合、ベタ書きした方が良い。

定義元を見に行くのがめんどくさいので、そのような場合はベタ書きした方が良いです。

オブジェクトのプロパティの型を追加したい場合、交差型を使う

交差型とは、&演算子を使って表現する型です。交差型を使うことで、柔軟にオブジェクトのプロパティの型を追加できます。

type User = {
  name: string;
};

type UserWithAge = User & {
  age: number;
};

const user: UserWithAge = {
  name: "yuki",
  age: 25,
};

APIのレスポンスのプロパティの型が必須になっていない場合、undefinedと型推論されてしまう

APIのレスポンスのプロパティの型が必須になっていない場合、undefinedと型推論されてしまいます。そのため、APIドキュメントを修正して必須にしておきましょう。そうすることで、undefinedを考慮せずに済みます。

setStateなどのセッターをpropsで渡す実装は避けた方が良い

コンポーネントで定義したsetStateなどのセッターを、子コンポーネントにpropsで渡す実装は避けた方が良いです。

■NG

// 子コンポーネント
export const SearchCandidates: FC<Props> = ({
  products,
  setProducts,
  setProduct,
}) => {

NGな理由

  • そもそもコンポーネントは自分自身にしか興味がないです。それなのになんで親コンポーネントからsetStateを受け取って、それで親コンポーネントのstateを更新してやらなきゃいけないのか?setStateを使ってどのようにstateを更新するかは親コンポーネントが持つべき情報であって、子コンポーネントが持つべきではないです。適切な場所に適切なコードを書くことによって、コンポーネント同士が密結合にならずに独立性を担保できます。独立性を担保することで、責務が適切に分離できていて変更に強くなったり、可読性やコンポーネントの再利用性が上がります。
  • useStateを定義したコンポーネント内で、どのようにstateが更新されるのかを把握することができないです。子コンポーネントを見に行かないといけないので、面倒です。

■OK

export const SearchCandidates: FC<Props> = ({
  products,
  onSelected,
}) => {

OKな理由

コンポーネントを切る際、コンポーネント名のディレクトリを作成してその配下にindex.tsxを作成する

コンポーネントを切る方法は2つ存在します。

  1. コンポーネント名.tsxを作成する
  2. コンポーネント名のディレクトリ/index.tsxを作成する

2番の方法はコンポーネント名のディレクトリにそのコンポーネントのテストをまとめることができるので、何かと嬉しいです。インポートする際もindex.tsxなので、パスを変えずにインポートできます(つまり、どちらも同じパスでインポートできます)。コンポーネントを切る場合は、どちらかの方法に統一しましょう。

カスタムフックで何でもかんでもまとめすぎない

そもそもカスタムフックとは、コンポーネントからロジックを抽出して再利用可能にした関数です。以下のuseProductsのようなカスタムフックの場合、名前から想像してAPIリクエストを発行してproductsおよびloadingを返すと考えられます。しかし、このuseProductsの返却値としてonSubmitやdisabledなど、使う側が想定できない値を返すのは良くありません。そして、onSubmitやdisabledなどはそのコンポーネントの都合で定義するものであって、カスタムフックに含めてしまうと、カスタムフックがそのコンポーネントに依存してしまい、カスタムフック自体の再利用性が落ちます。適切にカスタムフックを切りつつ、コンポーネントの都合で定義するものはコンポーネント内に定義しましょう。コンポーネントからロジックを分離しようとして、カスタムフックで何でもかんでもまとめるのではなく、①再利用性があるのか、②使う側にとって自然か、違和感がないかを考えてカスタムフックを作成しましょう。

■before

  const { products, disabled, onSubmit } = useProducts();

■after

  const { products } = useProducts();

  const disabled = // 省略
  const onSubmit = // 省略

カスタムフックを呼び出す際にonSuccessを渡すのではなく、カスタムフックが返すmutate関数を呼び出す際にonSuccessを渡すようにする

カスタムフックを呼び出す際にonSuccessを渡すのはやめましょう。メソッドを呼び出す際にonSuccessやonErrorを指定する方が違和感がないためです。また、現状、使う人が想定できないインターフェースになっています。「なぜカスタムフックを呼び出す際にonSuccessを渡さないといけないのか?」を定義元を見ないと分からず、使いづらいです。useMutationのmutate関数は呼び出す際にonSuccessやonErrorを指定できるようになっています。そのため、mutate関数を呼び出す際にonSuccessやonErrorを指定すればOKです。

■before

type Props = {
  onSuccess?: (data: PostProducts201Response) => void;
  onError?: () => void;
}

export const useCreateProduct = ({ onSuccess, onError }: Props) => {
  const api = // 省略

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
    {
      onSuccess,
      onError,
    },
  );

  return {
    create,
  };
};

■after

export const useCreateProduct = () => {
  const api = // 省略

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
  );

  return {
    create,
  };
};

カスタムフックのユースケースを制限するようなロジックをカスタムフックに含めない

ユースケースを制限するようなロジックは、大体カスタムフックを使う側の都合で定義されるロジックなので、そういうロジックはカスタムフックを使う側のコンポーネントに直接書いて、カスタムフックのユースケースを制限しないように気をつけましょう。

React Hook FormのvaluAsDateを利用して、Dateオブジェクトのインスタンスを作成する

valueAsDateを使うことで、ユーザーが入力した日付をDateオブジェクトのインスタンスに変換できます。APIリクエスト出す際にDateオブジェクトのインスタンスに自前で変換するのではなく、valueAsDateでユーザーが入力した瞬間に自動で変換するようにしましょう。

コンポーネント内で考慮すべきではないロジックを子コンポーネントに含めない

コンポーネント内で考慮すべきではないロジック(親コンポーネントの都合で決まるロジックなど)を子コンポーネントに含めると、子コンポーネントと親コンポーネントが依存してしまい、子コンポーネントの再利用性が下がります。そして、親コンポーネントが持つべき情報を子コンポーネントが持ってしまっているので、責務が正しく分離されていません。責務が正しく分離されていないと、子コンポーネント内で複雑なロジックを描く必要性も出てくるので、責務は適切に分離した方が良いです。

関数で1行の処理で実行結果だけをreturnする場合、アローの隣にリターンする値を書く

■before

  const { mutate: create } = useMutation(
    (data: Data) => { return api.postProducts(data); },
  );

■after

  const { mutate: create } = useMutation(
    (data: Data) => api.postProducts(data)
  );

APIから取得したデータをそのまま使うのではなく、フロント側で定義したモデルを使ってインスタンスを作成する

そもそもモデルとはなんなのかwikipediaで見てみましょう。

そのアプリケーションが扱う領域のデータと手続き(ビジネスロジック - ショッピングの合計額や送料を計算するなど)を表現する要素である。また、データの変更をビューに通知するのもモデルの責任である(モデルの変更を通知するのにObserver パターンが用いられることもある)。

フロントエンドのアプリケーションでAPIから取得したデータをそのまま使うと柔軟性が低いです。フロントエンドのアプリケーションなのにデータがAPIの型に依存しています。フロントエンドではフロントエンドに特化したデータの持ち方をしたいので、フロントエンドでもモデルを使用します。このモデルも基本的にはAPIの型に準拠するのですが、モデルを作ることによって、「データ」と「データに関する操作」をまとめられます。そしてフロントエンドに特化したデータの持ち方もできます(プロパティを増やしたり)。モデルを使わない場合、無秩序に色々な場所に「データ」と「データに関する操作」が定義されていて管理しづらいかつ変更に弱いです。

以下ではAPIのレスポンスをモデルを通してインスタンス化しています。こうすることで、フロントエンドのアプリケーションでデータとデータに関する操作を一元管理できます。

■before

  const { data, isLoading } = useQuery(["products"], () => api.getProducts());

■after

  const { isLoading } = useQuery(["products"], () => api.getProducts(), {
    onSuccess: ({ data: { products } }) => {
      setProducts(products.map((product) => new Product(product)));
    }
  });

↓ フロントエンドにおけるモデル

export class Product implements api.Product {
  [immerable] = true;

  id = "";
  name = "";
  nameEn = "";
  sku = "";

  constructor(data: Partial<Product> = {}) {
    Object.assign(this, data);
  }
}

フォームの値をそのまま投げたい

onSubmit関数の中で、フォームの値を整形して、APIリクエスト出すのはめんどくさいかつ複雑になってしまうので、onSubmitの中でフォームの値をなるべく整形しないでAPIリクエスト出せるようにInput要素にvalueAsNumberをつけたり、zodのtransformで整形しましょう。

複数のスプレッド構文を使っている場合、スプレッド構文の順番に気をつける

スプレッド構文の順番が違うだけで、プロパティの値が変わるので気をつけましょう。

const originalUser = { name: "yuki" };
const defaultUser = { name: "hoge" };

const userA = {
  ...originalUser,
  ...defaultUser,
};
// => { "name": "hoge" } 

const userB = {
  ...defaultUser,
  ...originalUser,
}; 
// => { "name": "yuki" }

bool値の変数の命名に気をつける

純粋に ~ かどうかを判定したい場合、is~で良いですが、~を表示したいかどうかを制御する場合は、show~の方が使う側がわかりやすいです。使う側にとって意味が分かりやすい命名をつけましょう。

NaNはnumber型

NaNはnumber型です。そのため、zodで以下のようにnumberとNaNのunion型にしても、priceはnumber型として考慮されます(型エイリアスでも同様です)。input numberで未入力を許容したいかつ値をnumber型として扱いたい場合、numberとNaNのunion型にすれば挙動を満たせるのでオススメです。初見の人には分かりづらいので、もしする場合はコメントを残しておくと良いでしょう。

↓ 型エイリアス

type Price = number | typeof NaN; // => type Price = number
// NOTE: 8個の商品で8個未満の商品を登録するために、デフォルト値のNaNを許容している
price: z.union([z.number().positive(), z.nan()]),

オブジェクトをスプレッド構文でpropsとして展開するのをやめる

props名が変数名に引きづられてしまい、props名を独自に定義するときに面倒なので、普通に書きます。普通に書いた方が自然です。

■before

<Product
  {...{name, age}}
/>

■after

<Product
  name={name}
  age={age}
/>

mutate関数の引数をオブジェクトリテラルでひとまとめにしない

関数を使う際に、引数をオブジェクトリテラルで囲むのは面倒であり、自然ではないです。なぜオブジェクトリテラルで囲むのかを定義元をみないといけないので、それも面倒です。

Propsにアロー関数を渡さない

Propsにアロー関数を渡さない方が良いのは、公式で推奨されています。

レンダー内でアロー関数を利用するとコンポーネントがレンダーされるたびに新しい関数が作成されるため、厳密な一致による比較に基づいた最適化が動作しなくなる可能性があります。

Propsにアロー関数を渡すことで、親コンポーネントが再レンダリングされた際にアロー関数が再生成されてしまい、その結果、propsが異なるので子コンポーネントが再レンダリングしてしまいます。不必要に子コンポーネントが再レンダリングしてしまうので、パフォーマンスが悪いです。Propsにはアロー関数を渡さず、事前に useCallback などを使用して関数を定義して、その関数を渡すようにしましょう。

return ( 
  <Todo
    onSelected={(item: Item) => setState( // 省略 )}
  />
)
const onSelected = useCallback((item: Item) => setState( // 省略 ), [])
return ( 
  <Todo
    onSelected={onSelected}
  />
)

終わり

単一責任の原則について調べていたら、wikipediaに「責務とは変更する理由である」と書いていて、すごくしっくりきたなと感じました。その言葉を知ってから、「なぜセッターを渡さなかったら責務が分離できるのか」を考えるとすごくしっくりくるなと感じました。

参考記事

コンポーネント(コンポーネントウェア)とは何か?コンポーネント指向や、その元になった「分割統治法」や「単一責任の原則」も含めて解説 | Promapedia(プロマペディア)

イラストで理解するSOLID原則 - Qiita

gapの余白指定が便利! gridとflexでできる新しいCSSレイアウト手法 - ICS MEDIA

useFieldArray | React Hook Form - Simple React forms validation

Zod | Documentation

TypescriptのEnum型の代わりにUnion型を使用する

イミュータブルが大事な理由、そしてImmerで簡単実現!

Classes | Immer

単一責任の原則 - Wikipedia

Reactのベストプラクティスを模索する - Qiita

JavaScript | Dateオブジェクトのインスタンスを作成する

独自フックの作成 – React

クラスを使わないアプリを実際に作ってみた気付き

React Hooksとカスタムフックが実現する世界 - ロジックの分離と再利用性の向上 - Qiita

コンポーネントに関数を渡す – React

SOLID原則で考えるReact設計

極端なパターンを考えて、技術的な落とし所を探っていく - mizdra's blog

【TS】Null合体演算子(??)についてざっくりまとめてみた

目次

背景

Null合体演算子を実務で使うことがあったのですが、パッと使うことができなかったので、ブログにまとめようと思います。

Null 合体演算子とは

Null合体演算子(??)とは、論理演算子の一種です。TSで代表的な論理演算子論理積演算子(&&)や論理和演算子(||)です。Null合体演算子は、基本的にはNull合体演算子の左辺の値を返しますが、左辺の値がnullまたはundefinedの場合に右辺の値を返します。

■defaultAgeがnullの場合

const originalDefaultAge = 25;

const defaultAge = null;

const user = {
  name: "yuki",
  age: defaultAge ?? originalDefaultAge,
};

console.log(user);
// => {
//      "name": "yuki",
//      "age": 25
//    }

■defaultAgeがundefinedの場合

const originalDefaultAge = 25;

const defaultAge = undefined;

const user = {
  name: "yuki",
  age: defaultAge ?? originalDefaultAge,
};

console.log(user);
// => {
//      "name": "yuki",
//      "age": 25
//    }

■defaultAgeが30の場合

const originalDefaultAge = 25;

const defaultAge = 30;

const user = {
  name: "yuki",
  age: defaultAge ?? originalDefaultAge,
};

console.log(user);
// => {
//      "name": "yuki",
//      "age": 30
//    }

Null合体演算子三項演算子で書く場合、記述が冗長になってしまうので、Null合体演算子で書ける場合は、Null合体演算子を使用した方が良いです。

const originalDefaultAge = 25;

const defaultAge = undefined;

const user = {
  name: "yuki",
  // Null合体演算子を使用する場合、defaultAge ?? originalDefaultAgeと書ける 
  age: (defaultAge === null) || (defaultAge === undefined) ? originalDefaultAge : defaultAge,
};

console.log(user);
// => {
//      "name": "yuki",
//      "age": 25
//    }

論理和演算子との違い

Null合体演算子は、論理和演算子の特殊形とみなすことができます。論理和演算子は、左辺の値がnullundefinedに関わらず、falsyな値の場合に、右辺の値を返します。

参考記事

Null 合体演算子 (??) - JavaScript | MDN

演算子 · JavaScript Primer #jsprimer

【Ruby】【TS】文字列を整数、整数を文字列に変換する方法をざっくりまとめてみた

目次

背景

文字列を整数、整数を文字列に変換する方法がパッと出なかったので、ブログにまとめようと思います。

文字列を整数に変換する

Ruby

Rubyで文字列を整数に変換するためには、String#to_iを使用します。

age = "25"

p age.to_i; #=> 25

数字が含まれていない文字列オブジェクトにString#to_iを使用した場合、全て数字の0に変換します。

age = ""

p age.to_i; #=> 0

TS

TSで文字列を整数に変換するためには、parseInt()またはNumberコンストラクタを使用します。 parseInt()の場合、数値を含む文字列から数字部分だけを取り出して文字列に変換することができるので、数値以外の余計な文字列が含まれた文字列を許容しています。Numberコンストラクタの場合、そのような文字列をNaNに変換するので、数値のみの文字列がちゃんと渡されていないことがわかります。そのため、 TSで文字列を整数に変換したい場合、Numberコンストラクタをメインで使用していきます。

// 数値以外の余計な文字列が含まれた文字列の場合
const age = "25歳";

console.log(parseInt(age)); // => 25
console.log(Number(age)); // => NaN
// 数値のみの文字列の場合
const age = "25";

console.log(parseInt(age)); // => 25
console.log(Number(age)); // => 25

数値を含まない文字列に使用した場合、parseInt()NumberコンストラクタNaNを返します。Rubyではそのような場合、0になるので、仕様が異なります。

const age = "あ";

console.log(parseInt(age)); // => NaN
console.log(Number(age)); // => NaN

整数を文字列に変換する

Ruby

Rubyで整数を文字列に変換する場合、Integer#to_sを使用します。

age = 25

p age.to_s # => "25"

もし、ageがnilの場合、to_sを実行するとNilClass#to_sが呼ばれます。NilClassnil のクラスです。 nil は NilClass クラスの唯一のインスタンスです。NilClass#to_sは空文字列を返します。

age = nil

p age.to_s # => ""

Rubyではto_s以外に、式展開で整数を文字列に変換できます。

age = 25

p "#{age}" # => "25"

TS

TSで整数を文字列に変換する場合、テンプレートリテラルを使うのがシンプルかつ覚えやすいです。

const age = 25;

console.log(`${age}`); // => "25" 

参考記事

String#to_i (Ruby 3.2 リファレンスマニュアル)

【Ruby】 to_iメソッドの使い方を理解しよう | Pikawaka

Number() コンストラクター - JavaScript | MDN

parseInt() - JavaScript | MDN

JavaScriptのparseInt()とNumber()の違い - Qiita

Integer#inspect (Ruby 3.2 リファレンスマニュアル)

NilClass#to_s (Ruby 3.2 リファレンスマニュアル)

【TS】【Ruby】配列に要素を追加する方法をざっくりまとめてみた

目次

背景

配列に要素を追加する方法を忘れそうになるので、ブログにまとめます。

配列に要素を追加する

Ruby

RubyではArray#pushメソッドを使用します。このpushメソッドは破壊的メソッドなので、元のオブジェクトを変更します。

nums = [1, 2]

nums.push(5)

p nums # => [1, 2, 5]

もし非破壊的に要素を追加したい場合、スプラット演算子を使用します。

nums = [1, 2]

new_nums = [*nums, 5]

p new_nums # => [1, 2, 5]
p nums # => [1, 2]

TS

TSではArray.prototype.push()メソッドを使用します。このpushメソッドは破壊的メソッドなので、元のオブジェクトを変更します。

const nums = [1, 2];

nums.push(5);

console.log(nums); // => [1, 2, 5] 

もし非破壊的に要素を追加したい場合、スプレッド構文を使用します。

const nums = [1, 2];

const newNums = [...nums, 5];

console.log(newNums); // => [1, 2, 5] 
console.log(nums); // => [1, 2] 

参考記事

Ruby と ECMAScript の配列展開の挙動の違い

Array#append (Ruby 3.2 リファレンスマニュアル)

Array.prototype.push() - JavaScript | MDN

【TS】【Ruby】日付をyyyy-mm-ddに変換する方法をざっくりまとめてみた

目次

背景

仕事で日付をyyyy-mm-ddに変換するのをやったときにパッとできなかったので、忘れないように記事にしようと思います。

日付をyyyy-mm-ddに変換する方法

Ruby

まずRubyで日付を生成するには、Dateクラスを使用します。Date.todayで今日の日付を持つDateクラスのインスタンスを生成できます。

require 'date'
today = Date.today

p today # => #<Date: 2022-12-04 ((2459918j,0s,0n),+0s,2299161j)>

この日付をyyyy-mm-ddに変換したい場合、Date#strftimeを使用します。 strftimeには文字列でフォーマットを指定します。フォーマットについて知りたい場合、Date#strftimeには詳しい説明が書いていなかったので、Time#strftimeを見た方が良いです。

require 'date'
today = Date.today.strftime("%Y-%m-%d")

p today # => "2022-12-04"

TS

まずTSで日付を生成するには、Dateオブジェクトを使ってインスタンスを生成します。Dateをnewするときにコンストラクタ引数を何も渡さない場合、作成されるインスタンスは現在の時刻を表すものになります。

const today = new Date();

console.log(today); // => Date: "2022-12-04T23:33:34.966Z" 

getMonth()メソッドは、月を0から11の整数で取得します。そのため、+ 1しています。

// YYYY/MM/DD形式の文字列に変換する関数
const formatDate = (date: Date) => {
  const yyyy = String(date.getFullYear());
   
  // Stringの`padStart`メソッド(ES2017)で2桁になるように0埋めする
  // 既に長さを満たしている場合、0埋めされない
  const mm = String(date.getMonth() + 1).padStart(2, "0");
  const dd = String(date.getDate()).padStart(2, "0");

  return `${yyyy}-${mm}-${dd}`;
}

const today = new Date();
console.log(formatDate(today)); // => "2022-12-05"

参考記事

class Date (Ruby 3.1 リファレンスマニュアル)

Date#strftime (Ruby 3.1 リファレンスマニュアル)

Time#strftime (Ruby 3.1 リファレンスマニュアル)

Date · JavaScript Primer #jsprimer

日付オブジェクト名.getMonth()-JavaScriptリファレンス