Node.jsのモダンなパターンを徹底解説!開発の質を高める秘訣!
引用元:https://news.ycombinator.com/item?id=44778936
うわー、これ知らなかったわ。Node.jsの実験的なパーミッション機能だって!Denoにインスパイアされたみたいで、めっちゃ良い機能じゃん。
# Run with restricted file system access
node –experimental-permission <br> –allow-fs-read=./data –allow-fs-write=./logs app.js
# Network restrictions
node –experimental-permission <br> –allow-net=api.example.com app.js
Looks like they were inspired by Deno. That’s an excellent feature. https://docs.deno.com/runtime/fundamentals/security/#permiss…
ランタイムやアプリにこんな機能は要らないって思うね。「適切な」場所はOSで、すでに解決済みだし、あらゆる角も処理されてる。車輪の再発明は複雑性、バグの表面、メンテナンスの負担を増やすだけだよ。他で解決済みの問題をなぜまた解決しようとするの?
正しく実装されてるとは思えないな。銀行が顧客を信頼するようなものだよ。MAC(強制アクセス制御)をやりたいなら、OSのカーネルレベルでやるべきだね。AppArmorやSELinuxを使えば、ファイルアクセス以外にもっと多くのことが制御できるよ。
Linux、macOS、Windows全体でこれをOSレベルでどう解決するつもり?Pythonプロジェクトで何年も良い方法を見つけようとしてきたんだ。試した解決策は互換性がないし、複雑でドキュメントも信頼できないから、ミスしそうで不安なんだよね。
「信頼できない」っていう不満、意味が分からないな。彼らがこのサポートをしない方が良かったってこと?何が言いたいわけ?単に信頼できないって言いたいだけ?
これをネイティブな方法でどうやるの?chrootとかかな?でも、みんなが*nixシステムを使ってるわけじゃないし、Node開発者がOSについてそこまで知ってるとも限らないよね。Javaエコシステムでもっとひどいけど、それは「一度書けばどこでも動く」って売りになってるし。
NodeはN-API経由でパッケージにネイティブアドオンを許可してるから、それらのパーミッションでは制限されないんだよね。Denoは–allow-ffiでこれに対応してるけど、この実験的なNodeのパーミッションはN-APIを無効化する機能がない。Node標準ライブラリしか制限できないみたい。
OSは実際には問題を解決してないよ。どんなプログラムでもファイルにアクセスできるし、そのアクセスを制御するのはかなり難しい。eBPFを使ってseLinuxでポリシーを強制するとか?それはマシンの管理者じゃないとできないことも多いしね。現代のセキュリティはサプライチェーン脆弱性対策で、Capabilityモデルみたいに、明示的に許可されたものしかアクセスできないようにするべきなんだ。
これはdotenvライブラリを使うことについての僕の考えなんだけど。「適切な」場所はOSだね。アプリは環境変数をロードするんじゃなくて、読み取るだけでいいんだよ。OMZとかにあるdotenv関数\プラグインの方がはるかに望ましいね。
嫌いな点なんてある?NodeのパーミッションはOSレベルの制限を置き換えるんじゃなくて、追加するだけなんだぜ。
NodeがN-API経由のネイティブアドオンを許可してるから、ネイティブモジュールはパーミッションに制限されないって?NodeパーミッションはNode標準ライブラリを制限するだけ。それがどうした?Nodeのドキュメントにhttps://nodejs.org/api/permissions.html#file-system-permissi…って明確に書いてあるじゃん。何が言いたいんだ?
OSレベルのチェックはOSやバージョンで変わるから、アプリバイナリにチェックがあればOS関係なく標準実装できるってことだね。DBレベルのセキュリティルールもよく聞くけど、RLSみたいな強力な機能を使ってもAPIレベルの認証チェックをサボっちゃダメ。ビジネスロジックとDB、両方で確認すべきだよ。
パス制限って簡単そうに見えて、ちゃんと実装するのは超ムズいんだぜ。PHPのopen_basedirも昔シンボリックリンクとかでバイパスされまくったらしいし。Nodeも同じ道を辿ってるみたいだね。DNSのトリックで–allow-net制限を突破される可能性もあるかも。これだけじゃ脆弱性じゃないだろうけど、標的型攻撃の一部にはなりうる。だから過信しないで、多層防御をしっかりやろうぜ!
いやいや、追加じゃなくて混乱させるだけだよ。管理者からしたら、同じ設定がいろんな場所でバラバラに管理されるのってマジで最悪なんだよな。Node.jsのこの機能実装は、システムレベルのいろんな細かいケース、特にDNS検索パスとか/etc/hosts
とか、考慮してないと思うわ。アプリが発見プロトコルに余計な(しかも壊れた)レイヤーを加えるのはやめてほしい。接続問題の解決がマジで大変になるんだから。
「*nixシステム以外で動く人もいる」ってWindowsのこと?WindowsにもOSレベルでちゃんとテストされて信頼できるファイルシステムパーミッションがあるぜ。「Node開発者全員がOSについて詳しいわけじゃない」って言うなら、このNode機能も理解できないだろうし、それに対応したコードも書けないだろうな。もしシステムパーミッションを真剣に考えるなら、OSでやる方がはるかに楽だって。
何年もcronがいいって言われてたけど、本番で動かなかったり、権限が足りなかったり、ログが残らなかったりって問題ばかりだったわ。OSの変更も大変。7年前に自作のNodeスクリプトに切り替えたら、マジでゼロ問題!開発者も喜んでスケジューラーに登録してる。DockerやKubernetesがあるのは、OS設定が必要なデプロイがムズい証拠。もし”npm start”でコードのバージョンに合った適切なパーミッション制限ができるなら、喜んで使うぜ!
必要なアクセス権だけを適切に設定してるアプリが、本番環境でどれくらいあると思う?その割合が高くても、開発者のマシンで何でもインポートするようなNodeスクリプト動かしてる人たちはどうなんだ?安全に動かすことはできるけど、そんな人は少ないだろうな。こういう機能があれば、その割合を増やせるんじゃないか?
Node.jsの実装ってDNS検索パスを認識してると思う?(俺の予想だと90%は認識してないね)。DNS検索パスってのは、/etc/resolv.conf
に”search foo”があると、foo.bar.com
とbar.com
へのリクエストが同じになる企業ネットワークの機能だよ。Node.jsがこれを理解せずに設定されたら、これらの操作はできなくなる。運用管理者としては、Node.jsのこんな機能を使われたらマジ嫌だ。おもちゃであって本物じゃない。DNSはアプリが扱うべきじゃなくて、システムのタスクなんだからさ。
簡単にバイパスできるパーミッションシステムなんて、何の意味があるんだ?
アプリと一緒にデプロイするには運用チームの協力が必要だけど、コマンドラインの変更は開発者ができるって話だよね。
OSレベルで解決するとOSごとに対応が必要だよね。Node.jsとJavaでデータ解析の方法が違うのと同じ。本当にOSに依存しない汎用的な解決策なら、ネットワークレベル、例えば特定のURLへのトラフィックだけ許可するプロキシを使うのがいいんじゃないかな。
実際的な理由としては、ランタイムはコードよりも多くのパーミッションを持つべきってことだね。例えばNode.jsのrequire(’fs’)
はシステムフォルダーのファイルを読み込む可能性があるから。
じゃあ、「セキュリティシアター」が一番良い選択肢なの?皮肉じゃなくて、すごく浅い選択肢で簡単に回避されちゃいそうだけど。
チームで開発するなら、env
や設定の読み込みロジックをリポジトリに組み込むとすごく便利だよ。アプリケーションプロセスで読み込む必要はなくて、コードベースの一部である周辺ツールに含めることができる。
allow-net
の公式ドキュメントが見つからないな。ブログ記事しかないみたい。
プロキシはネットワークアクセスは解決するけど、ファイルシステムアクセスは解決しないよね。プロキシを使う場合、信頼できないコードがプロキシ経由でしかネットワークにアクセスしないようにどう保証するかが課題だね。コンテナやiptables以外でその方法を見たことがないな。
cronが使えないなら、ランタイムやアプリ内ではなく、SaaS cronやSystemdなど既存のOSレベルの解決策を使うべきだ。DIYは「Do One Thing」に反し、不完全で非効率な上に、構築や長期保守に余計な時間がかかり、ビジネスに集中できなくなるという損失を生むよ。
まったく!「でもWindowsは…」って反論はよく聞くけど、Windowsがenv
などに欠けてるなら、サポートしてる環境に移行するか、Windowsユーザー専用のツールを導入すべきだ。複雑な.env
ファイルスキャナーなんて作るなよ。開発では.env
ファイルを使うけど、プロジェクト外のzenv
やloadenv
ツールを使って環境変数に読み込んでるよ。
開発者のコードにセキュリティホールがないか信用できないからね。
このアイデアはいいんだけど、OSツールが良くない場合はどうする?macOSとか、OSレベルのサンドボックスはあるけど、ドキュメントがほとんどなくて、ブログ記事を読み漁るしかないんだよ。Nodeに組み込めば、少なくとも理論的にはどのOSでも同じものがすぐに手に入るってことだよね。
[0] https://www.karltarvas.com/macos-app-sandboxing-via-sandbox-…
もっとコメントを表示(1)
ここでのキラーアップグレードはESMじゃないね。NodeがfetchとAbortControllerをコアに組み込んだことだよ。axiosやnode-fetchを捨てたら、Lambdaのバンドルが軽くなって、コールドスタートのレイテンシーが約100ms短縮されたんだ。もし習慣でまだnpm install axiosしてるなら、2025年のNodeはもう「補助輪を外す」合図だよ。
話はそれるけど、バリデーションとAPI呼び出しは密接だから共有するね。個人的にはts-rest
をスタック全体で使うのが好きだよ。zodやJSON Schemaベースのライブラリの中で一番スリムだし。好きなHTTPクライアントを使えるし(自分はBunやFastifyを使ってる)。型安全性をコンパイル時にほとんど移行できるから、多少のオーバーヘッドは全然気にならない。他の人はどう思う?何か他にオプションあるかな?かなり探したけど、軽量で十分な型安全性を備えてるのはこれだけだった気がする。
node fetchはaxiosより断然良いよ(使いやすくて分かりやすい、シンプル)。まだaxiosを使ってる人がいるなんて知らなかったな。
fetchの構文やresponse.json
をawaitする必要がある点、追加のエラーハンドリングが必要なところがどうも好きじゃなかったな。
async function fetchDataWithAxios() {
try {
const response = await axios.get(’https://jsonplaceholder.typicode.com/posts/1’);
console.log(’Axios Data:’, response.data);
} catch (error) {
console.error(’Axios Error:’, error);
}
}
async function fetchDataWithFetch() {
try {
const response = await fetch(’https://jsonplaceholder.typicode.com/posts/1’);
if (!response.ok) { // Check if the HTTP status is in the 200-299 range
throw new Error(”HTTP error! status: ${response.status}”);
}
const data = await response.json(); // Parse the JSON response
console.log(’Fetch Data:’, data);
} catch (error) {
console.error(’Fetch Error:’, error);
}
}
リリースから16年も経って、ネットワークリクエストを中心に据えたJSランタイムが、ようやくデフォルトでネットワークリクエストをサポートするようになったってことだね。
ライブラリ作者としては逆だね。fetchは素晴らしいけど、ESMは苦痛だったけど絶対的に価値のあるアップグレードだったよ。記事の作者が言ってること全部当てはまるし。
ライブラリ作者の視点は興味深いね。確かに、デュアルパッケージハザードとかCJS/ESM互換性地獄、ツール変更とか、エコシステム全体の変化に対応しなきゃいけなかったんだもんな。だから、ESMの方が彼らにとっては大きな出来事だったってのも分かるよ。
当たり前だけど、当時からネットワークリクエストはサポートしてたよ。fetch APIなんてまだ存在しなかったし、当時の標準だったXMLHttpRequestは狂ってたからね。
すごかったけどちゃんと動いたね。少なくともダウンロードの進捗は確認できたし。
fetchでダウンロード進捗は取れるけど、アップロードはドキュメント不足で実装が大変そうだよ。XMLHttpRequestの方がいいかもね。ちょっと興味湧いたから今から実装試してみるわ。
俺はいつもfetchはこう書くんだよね
const data = (await fetch(url)).then(r => r.json())
でもさ、好きなように構文をラップするのはめちゃくちゃ簡単だよね。
APIコールの型安全はめちゃくちゃ重要だよね。ts-restは使ったことないけど、コンパイル時バリデーションって堅実そう。実行時のサプライズより全然良いじゃん。実際、使ってみてどう?シンプルなエンドポイントだとスキーマ定義のオーバーヘッドって重く感じる?
axiosのエクステンションが恋しいなぁ。レート制限とか、スロットリング、リトライ戦略、キャッシュ、ロギングとかがめちゃくちゃ簡単に追加できたんだよね。fetchでもできるけど、なんかバラバラだし、ボイラープレートが多いんだよなぁ。
先週、俺もts-restを導入しようとしたんだよ。でも、express v5をまだサポートしてないって気づいてさ: https://github.com/ts-rest/ts-rest/issues/715
ts-restはいいライブラリだけど、メンテ不足で不安になっちゃったんだよね。LLMのおかげで今は自社製ソリューションを作るのもアリだよって話。俺はシンプルなts-restを数日で自作して大満足。Claudeがめちゃくちゃ助けてくれたわ。2025年には自作がもっと一般的になるかもね。
プラットフォームがネイティブなHTTPクライアントサポートを最初から持ってなかったことにいつも驚いてるんだよね。過去20年間、ほぼ全てのプロジェクトで必要だったのにさ。
あと、「fetch」って名前もさ、ほとんどのAPIコールがPOSTだって考えるとイマイチだよね。
俺は信頼性が必要なコードだと、APIコールには必ず何かしらのスキーマバリデーションを入れるようにしてるんだ。プロトタイプにはtRPCを使うこともあるけど、プロダクションではzodが一番使いやすいね。fetchApiみたいなラッパーでスキーマとバリデーションをまとめて扱うようにしてるよ。
なんでこれでダメなの?
const data = await (await fetch(url)).json()
fetchをベースにしたaxiosみたいなライブラリを作る余地がありそうだね。
CJS/ESMの互換性の問題が解消しつつあるってことは、あれって技術的な制約じゃなくて設計の問題だったんだな。たくさんの時間を無駄にしたわ。
向こう側でスキーマってどうやって提供してる?フロントエンドとバックエンドを同期させるのが大変だったから、バックエンドからスキーマを読んでフロントエンドにAPIファイルを生成するスクリプトを書いたよ。
すごく簡潔だね。でも、二重のawait
はやっぱり変だよな。なんで必要なんだ?
axios
はまだ、dev.toとかの初心者向けチュートリアルで使われてるのを見るよな。レガシーコードもたくさんあるし。
アップロードもダウンロードもプログレスバー付きで動かせたよ。サーバーがHTTP/2かQUICに対応してるかが鍵で、Express
じゃなくFastify
に乗り換えたらスムーズだった。fetch
が簡単に進捗追跡できるのは嬉しいね。
俺もライブラリをメンテしてるんだけど、ESMへの移行はめちゃくちゃ大変だったよ。結局CJSも配布しなきゃいけないし、どっちでもバンドルできてテストできるようなコードの書き方を考えなきゃならないからな。
AIがそれを80年代のディスコでWham.を流すみたいにぶり返させるぞ。「やるなら間違ってやれ」ってな。
gist見せてもらえる?
そうだね、典型的なバンドルサイズとDXのトレードオフだよね。fetch
は確かにボイラープレートが多いし、response.ok
の手動チェックとか二重のawait
はめんどくさい。Lambda
でコールドスタートを最適化するなら我慢するけど、バンドルサイズがそんなに重要じゃない普通のアプリ開発なら、axios
のきれいなAPIの方が俺は好きだな。
それは設計上の選択でも技術的な制約でもなかったんだ。複雑で、個別のグループ間の細かい内部作業と調整が必要なことだった。Joyee Cheungがそのすべてをやり遂げるために、かなり英雄的な努力をしたから実現したんだよ。Joyeeのブログ記事で詳しく説明されてるから、Node.jsみたいな大きなプロジェクトで物事がどう進むか、より正確な全体像がわかるよ: https://joyeecheung.github.io/blog/2024/03/18/require-esm-in…
ts-restは最近あまり使われてないらしいよ。俺たちはモダンなTanStack Queryとの連携がイケてないから、別の選択肢を探し始めたんだ。で、oRPCが使えるレベルになってたから、これをお勧めするよ。tRPCみたいだけど、ts-rest風の契約に対応してて、標準OpenAPI RESTエンドポイントも使えるんだ。チェックしてみて!
https://orpc.unnoq.com/
https://github.com/unnoq/orpc
未だにfetch
じゃなくてaxios
が使われてるのを見るたび、まじでムカつくね。みんな気にしてないのか、既存のプロジェクトをコピペして使い回してるだけなんだろうな。
もっとコメントを表示(2)
fetch
のネイティブなパフォーマンスとaxios
みたいな便利さが、まさに求めてるものだよね。そういう方向に進んでるライブラリもあるけど、まだ完璧なのはないな。多分、軽量さを保ちつつ、リトライパッケージの問題とか解決するのが難しいんだろうね。
もうchalk
とかpicocolors
をインストールする必要ないぜ。node:util
のstyleText
を使えば自分でテキストにスタイルをつけられるんだ。
Docs: https://nodejs.org/api/util.html#utilstyletextformat-text-op…
俺はそういうの全然必要なかったな。アプリ全体で使うオブジェクトプロパティに、こんな感じでANSIエスケープシーケンスを直接入れてたよ。text: { angry: ”\u001b[1m\u001b[31m”, ... }
こうやって直接呼んでたんだ。${vars.text.green}whatever${vars.text.none}
ってね。
それこそ、賢いつもりになってる人の問題点だよね。ターミナル設定に関係なくエスケープシーケンスが出力されちゃうじゃん。そんな時は、それをちゃんと処理してくれるライブラリ(他にもたくさんの癖に対応してくれる)を使う方が、ずっと理にかなってるよ。
賢いとかそういう問題じゃないね。エスケープシーケンスは何十年も前からあるし、事実上の標準として普遍的にサポートされてる。ChromiumのDevToolsのコンソールでさえ直接サポートされてるんだぜ。本当の問題は「自前主義(invented here syndrome)」だよ。みんな、不確実性への感情的な恐怖から、不合理にライブラリに頼りすぎなんだ。left-pad
事件みたいに極端な例を見ると、みんなが不合理な理由で依存関係を増やしてるのがわかるよ。
俺もマイクロライブラリは嫌いだけど、あんたの解決策だと、出力がgrep
とかにパイプされた時に、不要なエスケープコードが出力されちゃうんだよな。対話モードでしか意味ないものならいいけど、UNIXシェルの一部として動くことを想定してない壊れたプログラムを散々見てきたからな。まぁ、出力が対話型シェルじゃない時に、エスケープコードの変数に空の文字列を代入すれば簡単に解決できるんだけどね。
pnpm
の依存関係ツリー表示は、|less
にパイプしようとすると俺のターミナル環境を壊すんだよね。JavaScript関連のツールで、こういう困った挙動がいくつか見られるな。多くのユーザーはそんな深いところまで依存関係を見ないか、もっと凝ったツールを使ってるんだろうけど。これって、今のJavaScriptエコシステムの状況をよく表してる症状だと思うよ。
それは文字列出力の問題じゃないんだよ。ターミナルエミュレータの問題だ。アプリケーションが、呼び出し元のターミナルやシェルのモードや挙動を知る必要なんてないんだ。他のstdoutに書き込む全アプリケーションにまったく同じことが言える。賢さなんて関係ない。でも、もし本当に他の理由でANSI記述子を避けたいなら、例えばカラー出力が好きじゃないとか、俺のアプリケーションにはANSI制御文字列の値を空文字列に置き換える「no-color」オプションがあるよ。
多くのアプリがisTTY
を使って出力を決めてるってこと知ってた?
ターミナルエミュレーターはパイプには関係ないよ。grep
はエスケープコードをそのまま検索しちゃうから、これは文字列出力の問題だね。自分でやるならちゃんとやるか、NO_COLOR
みたいなエッジケースも考慮した実績のあるライブラリを使おうよ。
「実績のある」ってどういう意味?JS開発者は「たくさんの人が使ってるから安全」って思いがちだけど、それはセキュリティを重視する組織とは考えが違うよ。NPMの多くのパッケージには安全性の評価がなくて、悪意のあるものが増えてるんだ。自分でミスしても、依存関係を無計画に増やすよりはマシな場合が多いんじゃないかな。
文字列整形ライブラリの話であって、レンダリングエンジンじゃないんだからさ。品質が気になるならGitHubでソース読めばいいじゃん。俺はいつもそうやって、自分で実装するかインストールするか決めてるよ。
アプリケーションが100%担当すべきことだよ。だから多くのプログラムには--color=auto
みたいなオプションがあって、出力先(ターミナルなら色付き、パイプなら色なしとか)に応じて最適な出力モードを推測するんだ。
それはアプリを使う人や環境によるよ。公開するならライブラリがいいし、社内とか決まった環境なら、こんな簡単な解決策なら余計な依存は不要だね(ただし、ライブラリで簡単に代替できるようなシンプルな場合だけだけど)。
広く実装されてるターミナルのエスケープシーケンスはもうよく知られてると思うけど、なんでこんなの毎回コピペしなきゃいけないんだろ?ログをファイルにパイプしてもエスケープコードが書き込まれるんでしょ?もっと楽にすればいいじゃん。
厳密に言えば、ライブラリを使うのも「全プロジェクトにコピー」してるって言えるんじゃない?
それって不必要に理屈っぽいよ。前のコメントの人は、Node.jsの標準ライブラリに入ってるから、わざわざライブラリをインストールしたりANSIエスケープの表をコピペしたりするのを思いとどまらせるって言ってるんだ。
俺は「ascii.txt」ってファイルに「本の絵文字」のブロック文字を用意してて、ログの先頭にコピペしてるよ。ログがうるさくならなくていい感じ。HNだと表示できないから、ここのページにリンクするね: https://www.piliapp.com/emojis/books/
それは本のエモジだけじゃなくて、もっとたくさんの記号があるからだよ。幾何学的なコードのdocstringを書くのが楽になるんだ。残りはここにあるぜ(HNだとフォーマットが崩れるからコピペしてみてくれ)。
正直、シェルスクリプトを手書きするときはそうしてるけど、もし内蔵されてるならなんでわざわざするんだ?
これは最高だぜ。この記事を読んで、個人的な小さいプロジェクトにすぐ使えることがいくつか分かったよ。
1. Node.jsには組み込みのテストサポートがあるから、Jestはもういらないみたいだ!
2. Node.jsには組み込みのウォッチサポートがあるから、nodemonもいらないみたいだ!
node:test
を試したけど、これは小規模プロジェクトやサードパーティ依存を減らしたいライブラリ作者には便利だね。でも、大規模アプリにはシンプルすぎるし、node:assert
はおもちゃみたいだから、最低でももっとちゃんとしたアサーションライブラリが必要だ。vitest
は”ただ動く”し、多くのTypeScript設定の面倒な部分を解決してくれる。Jestは自重で潰れたよ。
何年もJestとかを避けてMochaのシンプルさを選んできた者として、Mochaがアサーションライブラリをテストハーネスと分離した設計は今でも評価してるぜ。Chaiは今も素晴らしいアサーションライブラリだ。node:test
プロジェクトでのTypeScript設定は、”type”: ”module”
やその他のおかげで、そんなに問題ないな。https://www.chaijs.com/
うーん、Node.jsのテスト機能はかなりイマイチだし、Node.jsの開発者たちは改善に興味がないみたいだぜ。数週間使ってみれば俺が言ってる意味が分かるはずだよ(それで問題点を報告しようとしても、Node.jsチームは気にしないって分かるだろうな)。
ドキュメントを見たところ、結構強力なモック機能やカスタムテストレポーターもあるみたいだね。これは本当に素晴らしい追加機能に聞こえるぜ。お前が言うように、実際に試すまでは熱意を抑えておくことにするよ。
それでも俺はMochaやChai、Sinon、Istanbulをインポートするよりは、そっちを使いたいね。結局ただのテストだし、構文が冗長でもLLMが書いてくれるんだからさ ;)