Node.js / TypeScript REST APIの構築、パート1:Express.js

公開: 2022-03-11

Node.jsでRESTAPIを作成するにはどうすればよいですか?

REST APIのバックエンドを構築する場合、多くの場合、Express.jsがNode.jsフレームワークの中で最初に選択されます。 静的HTMLとテンプレートの作成もサポートしていますが、このシリーズでは、TypeScriptを使用したバックエンド開発に焦点を当てます。 結果として得られるRESTAPIは、フロントエンドフレームワークまたは外部バックエンドサービスがクエリできるものになります。

あなたは必要になるでしょう:

  • JavaScriptとTypeScriptの基本的な知識
  • Node.jsの基本的な知識
  • RESTアーキテクチャの基本的な知識(必要に応じて、以前のREST API記事のこのセクションを参照)
  • Node.jsの準備ができたインストール(できればバージョン14以降)

ターミナル(またはコマンドプロンプト)で、プロジェクト用のフォルダーを作成します。 そのフォルダーから、 npm initを実行します。 これにより、必要な基本的なNode.jsプロジェクトファイルがいくつか作成されます。

次に、Express.jsフレームワークといくつかの便利なライブラリを追加します。

 npm i express debug winston express-winston cors

これらのライブラリがNode.js開発者のお気に入りであるのには十分な理由があります。

  • debugは、アプリケーションの開発中にconsole.log()を呼び出さないようにするために使用するモジュールです。 このようにして、トラブルシューティング中にデバッグステートメントを簡単にフィルタリングできます。 手動で削除する代わりに、本番環境で完全にオフにすることもできます。
  • winstonは、APIへのリクエストと、返されたレスポンス(およびエラー)をログに記録する責任があります。 express-winstonはExpress.jsと直接統合されているため、すべての標準API関連のwinstonロギングコードはすでに実行されています。
  • corsは、クロスオリジンリソースシェアリングを有効にすることができるExpress.jsミドルウェアの一部です。 これがないと、APIは、バックエンドとまったく同じサブドメインから提供されるフロントエンドからのみ使用できます。

バックエンドは、実行時にこれらのパッケージを使用します。 ただし、TypeScript構成の開発依存関係もいくつかインストールする必要があります。 そのために、以下を実行します。

 npm i --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript

これらの依存関係は、Express.jsやその他の依存関係で使用されるタイプとともに、アプリ自体のコードでTypeScriptを有効にするために必要です。 これにより、コーディング中に一部の関数メソッドを自動的に完了することができるため、WebStormやVSCodeなどのIDEを使用しているときに多くの時間を節約できます。

package.jsonの最終的な依存関係は、次のようになります。

 "dependencies": { "debug": "^4.2.0", "express": "^4.17.1", "express-winston": "^4.0.5", "winston": "^3.3.3", "cors": "^2.8.5" }, "devDependencies": { "@types/cors": "^2.8.7", "@types/debug": "^4.1.5", "@types/express": "^4.17.2", "source-map-support": "^0.5.16", "tslint": "^6.0.0", "typescript": "^3.7.5" }

必要な依存関係がすべてインストールされたので、独自のコードの作成を始めましょう。

TypeScriptRESTAPIプロジェクト構造

このチュートリアルでは、次の3つのファイルのみを作成します。

  1. ./app.ts
  2. ./common/common.routes.config.ts
  3. ./users/users.routes.config.ts

プロジェクト構造の2つのフォルダー( commonusers )の背後にある考え方は、独自の責任を持つ個々のモジュールを持つことです。 この意味で、最終的には、モジュールごとに次の一部またはすべてを使用することになります。

  • APIが処理できるリクエストを定義するためのルート構成
  • データベースモデルへの接続、クエリの実行、特定のリクエストに必要な外部サービスへの接続などのタスクのためのサービス
  • ルートの最終コントローラーがその詳細を処理する前に、特定の要求検証を実行するためのミドルウェア
  • データの保存と取得を容易にするために、特定のデータベーススキーマに一致するデータモデルを定義するためのモデル
  • 最終的に(ミドルウェアの後で)ルート要求を処理し、必要に応じて上記のサービス関数を呼び出し、クライアントに応答するコードからルート構成を分離するためのコントローラー

このフォルダー構造は、基本的なREST API設計、このチュートリアルシリーズの残りの部分の初期の開始点、および練習を開始するのに十分なものを提供します。

TypeScriptの一般的なルートファイル

commonフォルダーに、次のようなcommon.routes.config.tsファイルを作成しましょう。

 import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } }

ここでルートを作成する方法はオプションです。 ただし、TypeScriptを使用しているため、ルートシナリオは、後で説明するように、 extendsキーワードで継承を使用する練習をする機会です。 このプロジェクトでは、すべてのルートファイルの動作は同じです。名前(デバッグ目的で使用します)とメインのExpress.js Applicationオブジェクトへのアクセスがあります。

これで、ユーザールートファイルの作成を開始できます。 usersフォルダーで、 users.routes.config.tsを作成し、次のようにコーディングを開始します。

 import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } }

ここでは、 UsersRoutesクラスをインポートし、 CommonRoutesConfigという新しいクラスに拡張しています。 コンストラクターを使用して、アプリ(メインのexpress.Applicationオブジェクト)と名前UsersRoutesをCommonRoutesConfigのコンストラクターに送信します。

この例は非常に単純ですが、スケーリングして複数のルートファイルを作成する場合、これは重複コードを回避するのに役立ちます。

このファイルに、ロギングなどの新しい機能を追加したいとします。 CommonRoutesConfigクラスに必要なフィールドを追加すると、 CommonRoutesConfigを拡張するすべてのルートがそのフィールドにアクセスできるようになります。

クラス間で同様の機能を実現するためのTypeScript抽象関数の使用

これらのクラス間で類似した機能(APIエンドポイントの構成など)が必要であるが、クラスごとに異なる実装が必要な場合はどうなりますか? 1つのオプションは、抽象化と呼ばれるTypeScript機能を使用することです。

UsersRoutesクラス(および将来のルーティングクラス)がCommonRoutesConfigから継承する非常に単純な抽象関数を作成しましょう。 すべてのルートにconfigureRoutes()という名前の関数(共通のコンストラクターから呼び出すことができるようにするため)を強制したいとします。 ここで、各ルーティングクラスのリソースのエンドポイントを宣言します。

これを行うために、 common.routes.config.tsに3つの簡単なものを追加します。

  1. このクラスの抽象化を有効にするための、 class行へのキーワードabstract
  2. クラスの最後にある新しい関数宣言、 abstract configureRoutes(): express.Application; 。 これにより、 CommonRoutesConfigを拡張するクラスは、そのシグネチャに一致する実装を提供するように強制されます。そうでない場合、TypeScriptコンパイラはエラーをスローします。
  3. this.configureRoutes();の呼び出しコンストラクターの最後で、この関数が存在することを確認できるためです。

結果:

 import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; }

そのため、 CommonRoutesConfigを拡張するクラスには、 express.Applicationオブジェクトを返すconfigureRoutes()という関数が必要です。 つまり、 users.routes.config.tsを更新する必要があります。

 import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } }

私たちが作ったものの要約として:

最初にcommon.routes.configファイルをインポートし、次にexpressモジュールをインポートします。 次に、 UserRoutesクラスを定義し、 CommonRoutesConfig基本クラスを拡張するようにします。これは、 configureRoutes()を実装することを約束することを意味します。

CommonRoutesConfigクラスに情報を送信するために、クラスのconstructorを使用しています。 これは、 express.Applicationオブジェクトを受け取ることを想定しています。これについては、次のステップでさらに詳しく説明します。 super()を使用して、 CommonRoutesConfigのコンストラクターにアプリケーションとルートの名前(このシナリオではUsersRoutes)を渡します。 ( super()は、 configureRoutes()の実装を呼び出します。)

ユーザーエンドポイントのExpress.jsルートの構成

configureRoutes()関数は、RESTAPIのユーザー用のエンドポイントを作成する場所です。 そこで、Express.jsのアプリケーションとそのルート機能を使用します。

app.route()関数を使用する際のアイデアは、コードの重複を回避することです。これは、明確に定義されたリソースを使用してREST APIを作成しているため、簡単です。 このチュートリアルの主なリソースはユーザーです。 このシナリオには2つのケースがあります。

  • API呼び出し元が新しいユーザーを作成したり、既存のすべてのユーザーを一覧表示したりする場合、エンドポイントは最初、要求されたパスの最後にusersを配置する必要があります。 (この記事では、クエリのフィルタリング、ページ付け、またはその他のそのようなクエリについては説明しません。)
  • 呼び出し元が特定のユーザーレコードに固有の処理を実行する場合、リクエストのリソースパスはusers/:userIdのパターンに従います。

Express.jsで.route()が機能する方法により、HTTP動詞をエレガントなチェーンで処理できます。 これは、 .get() .post()がすべて、最初の.route()呼び出しと同じIRouteのインスタンスを返すためです。 最終的な構成は次のようになります。

 configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // this middleware function runs before any request to /users/:userId // but it doesn't accomplish anything just yet--- // it simply passes control to the next applicable function below using next() next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; }

上記のコードを使用すると、RESTAPIクライアントはPOSTまたはGETリクエストを使用してusersエンドポイントを呼び出すことができます。 同様に、クライアントはGETPUTPATCH 、またはDELETEリクエストを使用して/users/:userIdエンドポイントを呼び出すことができます。

ただし、 /users/:userIdには、 all()関数を使用する汎用ミドルウェアも追加しました。この関数は、 get()put()patch() 、またはdelete()関数の前に実行されます。 この機能は、(シリーズの後半で)認証されたユーザーのみがアクセスすることを目的としたルートを作成する場合に役立ちます。

.all()関数には、他のミドルウェアと同様に、 RequestResponseNextFunctionの3種類のフィールドがあることに気付いたかもしれません。

  • リクエストは、Express.jsが処理されるHTTPリクエストを表す方法です。 このタイプは、ネイティブNode.jsリクエストタイプをアップグレードおよび拡張します。
  • 応答も同様に、Express.jsがHTTP応答を表す方法であり、ネイティブのNode.js応答タイプを拡張します。
  • それほど重要ではありませんが、 NextFunctionはコールバック関数として機能し、制御が他のミドルウェア関数を通過できるようにします。 途中で、コントローラーが最終的にリクエスターに応答を送り返す前に、すべてのミドルウェアが同じ要求オブジェクトと応答オブジェクトを共有します。

Node.jsエントリポイントファイル、 app.ts

いくつかの基本的なルートスケルトンを構成したので、アプリケーションのエントリポイントの構成を開始します。 プロジェクトフォルダのルートにapp.tsファイルを作成し、次のコードで始めましょう。

 import express from 'express'; import * as http from 'http'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug';

記事のこの時点で、これらのインポートのうち新しいものは2つだけです。

  • httpはNode.jsネイティブモジュールです。 Express.jsアプリケーションを起動する必要があります。
  • body-parserは、Express.jsに付属するミドルウェアです。 制御が独自のリクエストハンドラーに渡される前に、リクエストを(この場合はJSONとして)解析します。

ファイルをインポートしたので、使用する変数の宣言を開始します。

 const app: express.Application = express(); const server: http.Server = http.createServer(app); const port = 3000; const routes: Array<CommonRoutesConfig> = []; const debugLog: debug.IDebugger = debug('app');

express()関数は、 http.Serverオブジェクトへの追加から始めて、コード全体で渡すメインのExpress.jsアプリケーションオブジェクトを返します。 ( express.Applicationを構成した後、 http.Serverを起動する必要があります。)

標準のポート80(HTTP)または443(HTTPS)は通常アプリのフロントエンドに使用されるため、これらは通常、ポート3000(TypeScriptが自動的にNumberであると推測します)でリッスンします。

なぜポート3000?

ポートを3000にするという規則はありません(指定しない場合は任意のポートが割り当てられます)が、Node.jsとExpress.jsの両方のドキュメントの例では3000が使用されているため、ここでは従来の方法を継続します。

Node.jsはフロントエンドとポートを共有できますか?

バックエンドが標準ポートでの要求に応答するようにしたい場合でも、カスタムポートでローカルに実行できます。 これには、特定のドメインまたはサブドメインを持つポート80または443でリクエストを受信するためのリバースプロキシが必要になります。 次に、それらを内部ポート3000にリダイレクトします。

以下に示すように、 routes配列はデバッグ目的でroutesファイルを追跡します。

最後に、 debugLogconsole.logに似た関数になりますが、より優れています。ファイル/モジュールコンテキストと呼びたいものに自動的にスコープされるため、微調整が簡単です。 (この場合、文字列でdebug()コンストラクターに渡すときに「アプリ」と呼びます。)

これで、すべてのExpress.jsミドルウェアモジュールとAPIのルートを構成する準備が整いました。

 // here we are adding middleware to parse all incoming requests as JSON app.use(express.json()); // here we are adding middleware to allow cross-origin requests app.use(cors()); // here we are preparing the expressWinston logging middleware configuration, // which will automatically log all HTTP requests handled by Express.js const loggerOptions: expressWinston.LoggerOptions = { transports: [new winston.transports.Console()], format: winston.format.combine( winston.format.json(), winston.format.prettyPrint(), winston.format.colorize({ all: true }) ), }; if (!process.env.DEBUG) { loggerOptions.meta = false; // when not debugging, log requests as one-liners } // initialize the logger with the above configuration app.use(expressWinston.logger(loggerOptions)); // here we are adding the UserRoutes to our array, // after sending the Express.js application object to have the routes added to our app! routes.push(new UsersRoutes(app)); // this is a simple route to make sure everything is working properly const runningMessage = `Server running at http://localhost:${port}`; app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(runningMessage) });

expressWinston.loggerはExpress.jsにフックし、完了したリクエストごとに、 debugと同じインフラストラクチャを介して詳細を自動的に記録します。 渡されたオプションは、対応するターミナル出力をきちんとフォーマットおよび色付けし、デバッグモードのときはより詳細なログ(デフォルト)を使用します。

expressWinston.loggerを設定した後、ルートを定義する必要があることに注意してください。

最後に、そして最も重要なこと:

 server.listen(port, () => { routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); // our only exception to avoiding console.log(), because we // always want to know when the server is done starting up console.log(runningMessage); });

これにより、実際にサーバーが起動します。 開始されると、Node.jsはコールバック関数を実行します。この関数は、デバッグモードで、構成したすべてのルートの名前を報告します。これまでのところ、 UsersRoutesのみです。 その後、コールバックは、本番モードで実行している場合でも、バックエンドがリクエストを受信する準備ができていることを通知します。

package.jsonを更新してTypeScriptをJavaScriptにトランスパイルし、アプリを実行します

スケルトンを実行する準備ができたので、最初にTypeScriptトランスパイルを有効にするためのボイラープレート構成が必要です。 プロジェクトルートにファイルtsconfig.jsonを追加しましょう。

 { "compilerOptions": { "target": "es2016", "module": "commonjs", "outDir": "./dist", "strict": true, "esModuleInterop": true, "inlineSourceMap": true } }

次に、次のスクリプトの形式でpackage.jsonに最後の仕上げを追加する必要があります。

 "scripts": { "start": "tsc && node --unhandled-rejections=strict ./dist/app.js", "debug": "export DEBUG=* && npm run start", "test": "echo \"Error: no test specified\" && exit 1" },

testスクリプトは、シリーズの後半で置き換えるプレースホルダーです。

startスクリプトのtscはTypeScriptに属しています。 TypeScriptコードをJavaScriptにトランスパイルし、 distフォルダーに出力します。 次に、 node ./dist/app.jsを使用してビルドバージョンを実行します。

--unhandled-rejections=strictをNode.jsに渡します(Node.js v16 +を使用している場合でも)。実際には、単純な「クラッシュしてスタックを表示する」アプローチを使用したデバッグは、 expressWinston.errorLoggerオブジェクトを使用したより簡単なロギングよりも簡単だからです。 これは本番環境でもほとんどの場合当てはまります。本番環境では、未処理の拒否にもかかわらずNode.jsを実行し続けると、サーバーが予期しない状態になり、さらに(より複雑な)バグが発生する可能性があります。

debugスクリプトはstartスクリプトを呼び出しますが、最初にDEBUG環境変数を定義します。 これには、すべてのdebugLog()ステートメント(および、同じdebugモジュールを使用するExpress.js自体からの同様のステートメント)を有効にして、有用な詳細を端末に出力できるようにする効果があります。標準のnpm startを使用した本番モードのサーバー。

npm run debugを自分で実行してみてください。その後、 npm startと比較して、コンソールの出力がどのように変化するかを確認してください。

ヒント: DEBUG=* DEBUG=appを使用して、デバッグ出力をapp.tsファイル自体のdebugLog()ステートメントに制限できます。 debugモジュールは一般的に非常に柔軟性があり、この機能も例外ではありません。

exportはMacとLinuxでどのように機能するかであるため、WindowsユーザーはおそらくexportSETに変更する必要があります。 プロジェクトが複数の開発環境をサポートする必要がある場合、cross-envパッケージはここで簡単なソリューションを提供します。

LiveExpress.jsバックエンドのテスト

npm rundebugまたはnpm start引き続きnpm run debugされると、REST APIはポート3000でリクエストを処理できるようになります。この時点で、cURL、Postman、Insomniaなどを使用してバックエンドをテストできます。

ユーザーリソースのスケルトンのみを作成したため、本文なしでリクエストを送信するだけで、すべてが期待どおりに機能していることを確認できます。 例えば:

 curl --request GET 'localhost:3000/users/12345'

バックエンドは、 GET requested for id 12345た回答を返送する必要があります。

POST ingについて:

 curl --request POST 'localhost:3000/users' \ --data-raw ''

これと、スケルトンを作成した他のすべてのタイプのリクエストは、非常によく似ています。

TypeScriptを使用したRapidNode.jsRESTAPI開発の準備ができています

この記事では、プロジェクトを最初から構成し、Express.jsフレームワークの基本に飛び込むことで、RESTAPIの作成を開始しました。 次に、 UsersRoutesConfigを拡張するCommonRoutesConfigを使用してパターンを構築することにより、TypeScriptをマスターするための最初のステップを踏み出しました。このパターンは、このシリーズの次の記事で再利用します。 最後に、 app.tsエントリポイントを構成して、新しいルートとpackage.jsonをスクリプトとともに使用して、アプリケーションをビルドおよび実行します。

しかし、Express.jsとTypeScriptで作成されたRESTAPIの基本でさえかなり複雑です。 このシリーズの次のパートでは、ユーザーリソース用の適切なコントローラーの作成に焦点を当て、サービス、ミドルウェア、コントローラー、およびモデルのいくつかの有用なパターンを掘り下げます。

プロジェクト全体はGitHubで入手でき、この記事の最後のコードはtoptal-article-01ブランチにあります。