ミドルウェア

Qwik City には、認証、セキュリティ、キャッシング、リダイレクト、ログ記録などのロジックを一元化してチェーン化できるサーバーミドルウェアが付属しています。ミドルウェアは、エンドポイントを定義するためにも使用できます。エンドポイントは、RESTful API や GraphQL API などのデータの返送に役立ちます。

ミドルウェアは、ルートによって定義された特定の順序で呼び出される一連の関数で構成されます。レスポンスを返すミドルウェアは、エンドポイントと呼ばれます。

ミドルウェア関数

ミドルウェアは、`src/routes` フォルダー内の `layout.tsx` または `index.tsx` ファイルで `onRequest`(または `onGet`、`onPost`、`onPut`、`onPatch`、`onDelete`)という名前の関数をエクスポートすることによって定義されます。

この例は、すべてのリクエストをログに記録する単純な `onRequest` ミドルウェア関数を示しています。

ファイル: `src/routes/layout.tsx`

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({next, url}) => {
  console.log('Before request', url);
  await next();
  console.log('After request', url);
};

特定の HTTP メソッドをインターセプトしたい場合は、これらのバリエーションのいずれかを使用できます。たとえば、`onRequest` と `onGet` の両方を使用する場合は、両方が実行されますが、チェーンでは `onRequest` が `onGet` より前に実行されます。

// Called only with a specific HTTP method
export const onGet: RequestHandler = async (requestEvent) => { ... }
export const onPost: RequestHandler = async (requestEvent) => { ... }
export const onPut: RequestHandler = async (requestEvent) => { ... }
export const onPatch: RequestHandler = async (requestEvent) => { ... }
export const onDelete: RequestHandler = async (requestEvent) => { ... }

各ミドルウェア関数は、RequestEvent オブジェクトを受け取ります。これにより、ミドルウェアはレスポンスを制御できます。

呼び出し順序

ミドルウェア関数チェーンの順序は、それらの位置によって決まります。特定のルートの最上位の `layout.tsx` から始まり、そのルートの `index.tsx` で終わります。(ルートパスによって定義されるレイアウトとルートコンポーネントの順序と同じ解決ロジックです。)

たとえば、リクエストが次のフォルダー構造で ` /api/greet/` の場合、呼び出し順序は次のとおりです。

src/
└── routes/
    ├── layout.tsx            # Invocation order: 1 (first)
    └── api/
        ├── layout.tsx        # Invocation order: 2   
        └── greet/
            └── index.ts      # Invocation order: 3 (last)

Qwik City は、各ファイルを順番に調べて、エクスポートされた `onRequest`(または `onGet`、`onPost`、`onPut`、`onPatch`、`onDelete`)関数が存在するかどうかを確認します。見つかった場合は、その順序で関数がミドルウェア実行チェーンに追加されます。

`routeLoader$` と `routeAction$` もミドルウェアの一部です。これらは `on*` 関数の後、デフォルトでエクスポートされたコンポーネントの前に実行されます。

コンポーネントを HTML エンドポイントとして

コンポーネントのレンダリングを暗黙的な HTML エンドポイントと考えることができます。そのため、`index.tsx` にデフォルトでエクスポートされたコンポーネントがある場合、そのコンポーネントはミドルウェアチェーンで暗黙的にエンドポイントになります。コンポーネントのレンダリングはミドルウェアチェーンの一部であるため、認証、ログ記録、その他のクロスカット懸念事項の一部として、コンポーネントのレンダリングをインターセプトできます。

import { component$ } from '@builder.io/qwik';
import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ redirect }) => {
  if (!isLoggedIn()) {
    throw redirect(308, '/login');
  }
};
 
export default component$(() => {
  return <div>You are logged in.</div>;
});
 
function isLoggedIn() {
  return true; // Mock login as true
}

RequestEvent

すべてのミドルウェア関数は、HTTP レスポンスのフローを制御するために使用できる `RequestEvent` オブジェクトを受け取ります。たとえば、クッキー、ヘッダーの読み取り/書き込み、リダイレクト、レスポンスの生成、ミドルウェアチェーンからの早期終了を行うことができます。ミドルウェア関数は、上記のように、最上位の `layout.tsx` から最後の `index.tsx` まで順番に実行されます。

next()

`next()` 関数を使用して、チェーン内の次のミドルウェア関数を実行します。これは、`next()` を明示的に呼び出さずにミドルウェア関数が正常に返された場合のデフォルトの動作です。`next()` 関数を使用して、次のミドルウェア関数のラッピング動作を実現できます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
// Generic function `onRequest` is executed first
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest start');
  await next(); // Execute next middleware function (onGet)
  log.push('onRequest end');
 
  json(200, log);
};
 
// Specific functions such as `onGet` are executed next
export const onGet: RequestHandler = async ({ next, sharedMap }) => {
  const log = sharedMap.get('log') as string[];
 
  log.push('onGET start');
  // execute next middleware function
  // (in our case, there are no more middleware functions nor components.)
  await next();
  log.push('onGET end');
};

一般的に、関数の正常な(例外ではない)戻りは、チェーン内の次の関数を実行します。しかし、関数からエラーをスローすると、実行チェーンが停止します。これは通常、認証または承認に使用され、401または403のHTTPステータスコードを返します。next()は暗黙的であるため、チェーン内の次のミドルウェア関数を呼び出さないようにするには、エラーをthrowする必要があります。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ next, sharedMap, json }) => {
  const log: string[] = [];
  sharedMap.set('log', log);
 
  log.push('onRequest');
  if (isLoggedIn()) {
    // normal behavior call next middleware
    await next();
  } else {
    // If not logged in throw to prevent implicit call to the next middleware.
    throw json(404, log);
  }
};
 
export const onGet: RequestHandler = async ({ sharedMap }) => {
  const log = sharedMap.get('log') as string[];
  log.push('onGET');
};
 
function isLoggedIn() {
  return false; // always return false as mock example
}

sharedMap

ミドルウェア関数間でデータを共有する方法としてsharedMapを使用します。sharedMapはHTTPリクエストのスコープを持ちます。一般的なユースケースは、他のミドルウェア関数、routeLoader$()、またはコンポーネントで使用できるように、ユーザーの詳細をsharedMapに格納することです。

import { component$ } from '@builder.io/qwik';
import {
  routeLoader$,
  type RequestHandler,
  type Cookie,
} from '@builder.io/qwik-city';
 
interface User {
  username: string;
  email: string;
}
 
export const onRequest: RequestHandler = async ({
  sharedMap,
  cookie,
  send,
}) => {
  const user = loadUserFromCookie(cookie);
  if (user) {
    sharedMap.set('user', user);
  } else {
    throw send(401, 'NOT_AUTHORIZED');
  }
};
 
function loadUserFromCookie(cookie: Cookie): User | null {
  // this is where you would check cookie for user.
  if (cookie) {
    // just return mock user for this demo.
    return {
      username: `Mock User`,
      email: `[email protected]`,
    };
  } else {
    return null;
  }
}
 
export const useUser = routeLoader$(({ sharedMap }) => {
  return sharedMap.get('user') as User;
});
 
export default component$(() => {
  const log = useUser();
  return (
    <div>
      {log.value.username} ({log.value.email})
    </div>
  );
});

headers

現在のリクエストに関連付けられたレスポンスヘッダーを設定するためにheadersを使用します。(リクエストヘッダーの読み取りについては、request.headersを参照してください。)ミドルウェアは、headersプロパティを使用して、レスポンスにレスポンスヘッダーを手動で追加できます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headers, json }) => {
  headers.set('X-SRF-TOKEN', Math.random().toString(36).replace('0.', ''));
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

リクエストのCookie情報を設定および取得するためにcookieを使用します。ミドルウェアは、cookie関数を使用して、Cookieを手動で読み書きできます。これは、JWTトークンなどのセッションCookie、またはユーザーを追跡するためのCookieを設定する場合に役立ちます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ cookie, json }) => {
  let count = cookie.get('Qwik.demo.count')?.number() || 0;
  count++;
  cookie.set('Qwik.demo.count', count);
  json(200, { count });
};

method

現在のHTTPリクエストメソッド(GETPOSTPATCHPUTDELETE)を返します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ method, json }) => {
  json(200, { method });
};

url

現在のHTTPリクエストURLを返します。(コンポーネントで現在のURLが必要な場合は、useLocation()を使用してください。urlはミドルウェア関数用です。)

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ url, json }) => {
  json(200, { url: url.toString() });
};

basePathname

アプリケーションがマウントされている現在のベースパスネームURLを返します。通常は/ですが、アプリケーションがサブパスにマウントされている場合は異なる場合があります。viteのqwikCity({root: '/my-sub-path-location'})を参照してください。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ basePathname, json }) => {
  json(200, { basePathname });
};

params

URLの「params」を取得します。たとえば、params.myIdを使用すると、このルート定義/base/[myId]/somethingからmyIdを取得できます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ params, json }) => {
  json(200, { params });
};

query

URLクエリパラメータを取得するためにqueryを使用します。(これはurl.searchParamsのショートハンドです。)ミドルウェア関数用に提供されており、コンポーネントはuseLocation() APIを使用する必要があります。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ query, json }) => {
  const obj: Record<string, string> = {};
  query.forEach((v, k) => (obj[k] = v));
  json(200, obj);
};

parseBody()

URLに送信されたフォームデータの解析にはparseBody()を使用します。

このメソッドは、リクエストヘッダーでContent-Typeヘッダーをチェックし、それに応じて本文を解析します。application/jsonapplication/x-www-form-urlencoded、およびmultipart/form-dataコンテンツタイプをサポートしています。

Content-Typeヘッダーが設定されていない場合、nullを返します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    `
      <form id="myForm" method="POST">
        <input type="text" name="project" value="Qwik"/>
        <input type="text" name="url" value="https://qwik.dokyumento.jp"/>
      </form>
      <script>myForm.submit()</script>`
  );
};
 
export const onPost: RequestHandler = async ({ parseBody, json }) => {
  json(200, { body: await parseBody() });
};

cacheControl

キャッシュヘッダーを設定するための便利なAPI。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({
  cacheControl,
  headers,
  json,
}) => {
  cacheControl({ maxAge: 42, public: true });
  const obj: Record<string, string> = {};
  headers.forEach((value, key) => (obj[key] = value));
  json(200, obj);
};

platform

デプロイメントプラットフォーム(Azure、Bun、Cloudflare、Deno、Google Cloud Run、Netlify、Node.js、Vercelなど)固有のAPI。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ platform, json }) => {
  json(200, Object.keys(platform));
};

locale()

現在のロケールを設定または取得します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onRequest: RequestHandler = async ({ locale, request }) => {
  const acceptLanguage = request.headers.get('accept-language');
  const [languages] = acceptLanguage?.split(';') || ['?', '?'];
  const [preferredLanguage] = languages.split(',');
  locale(preferredLanguage);
};
 
export const onGet: RequestHandler = async ({ locale, json }) => {
  json(200, { locale: locale() });
};

status()

レスポンスのステータスをレスポンスの書き込みとは独立して設定します。ストリーミングに役立ちます。エンドポイントは、status()メソッドを使用して、レスポンスのHTTPステータスコードを手動で変更できます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ status, getWritableStream }) => {
  status(200);
  const stream = getWritableStream();
  const writer = stream.getWriter();
  writer.write(new TextEncoder().encode('Hello World!'));
  writer.close();
};

redirect()

新しいURLにリダイレクトします。他のミドルウェア関数の実行を防ぐためにスローすることの重要性に注意してください。redirect()メソッドは、リダイレクトURLにLocationヘッダーを自動的に設定します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ redirect, url }) => {
  throw redirect(
    308,
    new URL('/demo/qwikcity/middleware/status/', url).toString()
  );
};

error()

エラーレスポンスを設定します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ error }) => {
  throw error(500, 'ERROR: Demonstration of an error response.');
};

text()

テキストベースのレスポンスを送信します。テキストエンドポイントを作成するには、text(status, string)メソッドを呼び出すだけです。text()メソッドは、Content-Typeヘッダーをtext/plain; charset=utf-8に自動的に設定します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ text }) => {
  text(200, 'Text based response.');
};

html()

HTMLレスポンスを送信します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ html }) => {
  html(
    200,
    ` 
      <html>
        <body>
          <h1>HTML response</h1>
        </body>
      </html>`
  );
};

json()

JSONエンドポイントを作成するには、json(status, object)メソッドを呼び出すだけです。json()メソッドは、Content-Typeヘッダーをapplication/json; charset=utf-8に自動的に設定し、データをJSON文字列化します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json }) => {
  json(200, { hello: 'world' });
};

send()

生のエンドポイントを作成するには、send(Response)メソッドを呼び出すだけです。send()メソッドは、標準のResponseオブジェクト(Responseコンストラクターを使用して作成できます)を受け取ります。

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({send}) => {
  const response = new Response('Hello World', {
    status: 200,
    headers: {
      'Content-Type': 'text/plain',
    },
  });
  send(response);
};

exit()

ミドルウェア関数の実行を停止するためにスローします。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ exit }) => {
  throw exit();
};

env

プラットフォームに依存しない方法で環境プロパティを取得します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ env, json }) => {
  json(200, {
    USER: env.get('USER'),
    MODE_ENV: env.get('MODE_ENV'),
    PATH: env.get('PATH'),
    SHELL: env.get('SHELL'),
  });
};

getWritableStream()

ストリームレスポンスを設定します。

import type { RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async (requestEvent) => {
  const writableStream = requestEvent.getWritableStream();
  const writer = writableStream.getWriter();
  const encoder = new TextEncoder();
 
  writer.write(encoder.encode('Hello World\n'));
  await wait(100);
  writer.write(encoder.encode('After 100ms\n'));
  await wait(100);
  writer.write(encoder.encode('After 200ms\n'));
  await wait(100);
  writer.write(encoder.encode('END'));
  writer.close();
};
 
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

headerSent

ヘッダーが設定されているかどうかを確認します。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ headersSent, json }) => {
  if (!headersSent) {
    json(200, { response: 'default response' });
  }
};
 
export const onRequest: RequestHandler = async ({ status }) => {
  status(200);
};

request

HTTPリクエストオブジェクトを取得します。ヘッダーなどのリクエストデータを取得するのに役立ちます。

import { type RequestHandler } from '@builder.io/qwik-city';
 
export const onGet: RequestHandler = async ({ json, request }) => {
  const obj: Record<string, string> = {};
  request.headers.forEach((v, k) => (obj[k] = v));
  json(200, { headers: obj });
};

貢献者

このドキュメントの改善に貢献してくれたすべての貢献者に感謝します!

  • adamdbradley
  • manucorporat
  • mhevery
  • CoralWombat
  • EamonHeffernan
  • lollyxsrinand
  • gparlakov
  • mrhoodz
  • harishkrishnan24
  • jemsco