OpenAPI specification の生成
TypeScript decorator を使用して、OpenAPI spec を自動生成し、REST API を公開します。
OpenAPI Endpoint を使用する理由
third-party integration、mobile client、または Squid Client SDK を使用できない external service 向けに、backend を標準 REST API として公開する必要があります。
OpenAPI がなければ、route を手動で定義し、spec file を作成し、CORS を構成し、documentation を同期し続ける必要があります。Squid の OpenAPI support を使用すると、method を decorate するだけで、完全に document 化された REST API を取得できます。
// Backend: just decorators on a service method
@Route('orders')
export class OrderService extends SquidService {
@Get('{orderId}')
async getOrder(@Path() orderId: string): Promise<Order> {
const order = await this.fetchOrder(orderId);
return this.createOpenApiResponse(order);
}
}
// Consumers call it as a standard REST endpoint:
// GET https://<your-app>.squid.cloud/openapi/orders/abc123
手動での spec 作成は不要です。route configuration も不要です。method を decorate するだけです。
概要
Squid は tsoa decorator を使用して、TypeScript code から OpenAPI specification を生成します。SquidService subclass の method として endpoint を定義すると、Squid が spec の生成、routing、API の提供を行います。
OpenAPI endpoint を使用する場合
| ユースケース | 推奨 |
|---|---|
| external consumer 向けに REST API を公開する | OpenAPI endpoint |
| Squid Client SDK から backend function を呼び出す | Executables を使用 |
| database の変更に反応する | Triggers を使用 |
| external service から HTTP callback を受信する | Webhooks を使用 |
仕組み
SquidServiceを拡張する class に@Routeを追加し、method に HTTP method decorator を追加します- Squid は deploy 時に decorated method を検出し、OpenAPI spec を生成します
- external consumer は標準 HTTP request を介して endpoint を呼び出します
- Squid が request を routing し、parameter を抽出して method を呼び出します
createOpenApiResponse()を使用して response を返します
クイックスタート
前提条件
squid initで初期化された Squid backend project- NPM からインストールされた
@squidcloud/backendpackage
ステップ 1: OpenAPI endpoint を作成する
service class に @Route decorator を追加し、method に HTTP method decorator を追加します。
import { SquidService } from '@squidcloud/backend';
import { Get, Query, Route } from 'tsoa';
@Route('example')
export class ExampleService extends SquidService {
@Get('echo')
async echo(@Query() message: string): Promise<string> {
return this.createOpenApiResponse(message);
}
}
ステップ 2: service を export する
service が service index file から export されていることを確認します。
export * from './example-service';
ステップ 3: backend を deploy する
squid deploy
ステップ 4: endpoint を呼び出す
curl "https://YOUR_APP_ID-dev.APP_REGION.squid.cloud/openapi/example/echo?message=hello"
response body は hello になります。
Endpoint URL
OpenAPI には 2 つの endpoint target があります。
OpenAPI endpoint の Specification
OpenAPI endpoint の spec を download するには、base URL に /openapi/spec.json を追加します。
Endpoint
各 endpoint は、base URL に /openapi/{route}/{method} template を追加した指定 route で利用できます。
Base URL
base URL は backend の実行方法によって異なります。
Local development
ローカルで開発する場合、endpoint は次の base URL を使用します。
https://[YOUR_APP_ID]-dev-[YOUR_SQUID_DEVELOPER_ID].[APP_REGION].squid.cloud
対応する endpoint は以下のとおりです。
https://[YOUR_APP_ID]-dev-[YOUR_SQUID_DEVELOPER_ID].[APP_REGION].squid.cloud/openapi/spec.json
https://[YOUR_APP_ID]-dev-[YOUR_SQUID_DEVELOPER_ID].[APP_REGION].squid.cloud/openapi/{route}/{method}
Deployed environment
deploy 済み environment の base URL は、development environment と production environment のどちらを使用するかによって異なります。development environment の URL には app ID の後に -dev が含まれるため、URL は少し異なります。
Dev:
https://[YOUR_APP_ID]-dev.[APP_REGION].squid.cloud
Prod:
https://[YOUR_APP_ID].[APP_REGION].squid.cloud
たとえば、Dev 用の対応する endpoint は次のとおりです。
https://[YOUR_APP_ID]-dev.[APP_REGION].squid.cloud/openapi/spec.json
https://[YOUR_APP_ID]-dev.[APP_REGION].squid.cloud/openapi/{route}/{method}
Monitoring
Squid Console の Backend tab 内にある OpenAPI で、OpenAPI controller、spec、usage を確認します。
コアコンセプト
HTTP method decorator
Squid は tsoa decorator を通じて、すべての標準 HTTP method をサポートします。
import { SquidService } from '@squidcloud/backend';
import { Body, Delete, Get, Patch, Path, Post, Put, Route } from 'tsoa';
interface Item {
id: string;
name: string;
price: number;
}
interface CreateItemRequest {
name: string;
price: number;
}
@Route('items')
export class ItemService extends SquidService {
@Get('{itemId}')
async getItem(@Path() itemId: string): Promise<Item> {
const item: Item = { id: itemId, name: 'Widget', price: 9.99 };
return this.createOpenApiResponse(item);
}
@Post()
async createItem(@Body() data: CreateItemRequest): Promise<Item> {
const newItem: Item = { id: crypto.randomUUID(), ...data };
return this.createOpenApiResponse(newItem, 201);
}
@Put('{itemId}')
async replaceItem(@Path() itemId: string, @Body() data: Item): Promise<Item> {
const updatedItem: Item = { ...data, id: itemId };
return this.createOpenApiResponse(updatedItem);
}
@Patch('{itemId}')
async updateItem(@Path() itemId: string, @Body() data: Partial<Item>): Promise<Item> {
const updatedItem: Item = { id: itemId, name: data.name ?? 'Widget', price: data.price ?? 9.99 };
return this.createOpenApiResponse(updatedItem);
}
@Delete('{itemId}')
async deleteItem(@Path() itemId: string): Promise<void> {
console.log(`Deleting item ${itemId}`);
return this.createOpenApiResponse(undefined, 204);
}
}
Parameter decorator
HTTP request のさまざまな部分から data を抽出します。
| Decorator | Source | 例 |
|---|---|---|
@Path() | URL path segment | /items/{itemId} |
@Query() | Query string | ?status=active |
@Body() | Request body | JSON payload |
@Header() | HTTP header | Authorization header |
@UploadedFile(fieldName) | 単一 file upload | Form file input |
@UploadedFiles(fieldName) | 複数 file upload | Multi-file form input |
@FormField() | Form field data | Form text input |
すべての decorator は tsoa から import します。
createOpenApiResponse()
custom status code と header を持つ response を構築するには、この method を使用します。
// 200 with body (default)
return this.createOpenApiResponse({ id: '123', name: 'Widget' });
// 201 Created with custom header
return this.createOpenApiResponse({ id: 'new-item' }, 201, { 'x-custom-header': 'created' });
// 204 No Content
return this.createOpenApiResponse(undefined, 204);
// 404 Not Found
return this.createOpenApiResponse({ error: 'Resource not found' }, 404);
Parameters:
| Parameter | Type | 説明 |
|---|---|---|
body | unknown(optional) | response payload |
statusCode | number(optional) | HTTP status code。body が存在する場合の default は 200、存在しない場合は 204 |
headers | Record<string, unknown>(optional) | Response header |
throwOpenApiResponse()
この method を使用すると、execution を即座に停止して response を返せます。これは authorization failure などの early exit に役立ちます。
@Get('protected')
async protectedEndpoint(): Promise<string> {
const apiKey = this.context.headers?.['x-api-key'];
if (!apiKey) {
this.throwOpenApiResponse({ body: 'UNAUTHORIZED', statusCode: 401 });
}
return this.createOpenApiResponse('Secret data');
}
OpenAPI context
すべての OpenAPI request では、this.context.openApiContext を通じて raw request detail にアクセスできます。
@Get('debug')
async debugRequest(): Promise<object> {
const ctx = this.context.openApiContext!;
return this.createOpenApiResponse({
method: ctx.request.method, // 'get', 'post', etc.
path: ctx.request.path, // '/debug'
queryParams: ctx.request.queryParams,
headers: ctx.request.headers,
rawBody: ctx.request.rawBody, // Raw request body string
});
}
File の処理
File の upload:
import { SquidService } from '@squidcloud/backend';
import { Post, Route, UploadedFile, UploadedFiles } from 'tsoa';
@Route('files')
export class FileService extends SquidService {
@Post('upload')
async uploadFile(@UploadedFile() file: Express.Multer.File): Promise<object> {
return this.createOpenApiResponse({
filename: file.originalname,
size: file.size,
mimetype: file.mimetype,
});
}
@Post('upload-multiple')
async uploadFiles(@UploadedFiles() files: Express.Multer.File[]): Promise<object> {
return this.createOpenApiResponse(files.map((f) => ({ name: f.originalname, size: f.size })));
}
}
File の返却:
response MIME type を指定するには、@Produces decorator を使用します。
import { SquidService } from '@squidcloud/backend';
import { Get, Produces, Route } from 'tsoa';
@Route('files')
export class FileDownloadService extends SquidService {
@Get('download')
@Produces('application/octet-stream')
async downloadFile(): Promise<File> {
const content = new Uint8Array([72, 101, 108, 108, 111]);
const file = new File([content], 'hello.txt', { type: 'text/plain' });
return this.createOpenApiResponse(file);
}
}
Spec documentation decorator
追加 metadata を使用して、生成された OpenAPI spec を拡張します。
| Decorator | 目的 | 例 |
|---|---|---|
@Tags('label') | spec 内の endpoint を group 化 | @Tags('Users') |
@Response(code, desc) | 使用可能な response code を document 化 | @Response(404, 'Not found') |
@Produces(mime) | response MIME type を指定 | @Produces('application/octet-stream') |
Authentication と Configuration
tsoa.json による API spec の設定
backend project root に tsoa.json file を作成し、spec generation をカスタマイズして security scheme を定義します。
{
"entryFile": "src/service/index.ts",
"noImplicitAdditionalProperties": "throw-on-extras",
"controllerPathGlobs": ["src/**/*.ts"],
"spec": {
"outputDirectory": "dist",
"specVersion": 3,
"securityDefinitions": {
"apiKeyAuth": {
"type": "apiKey",
"name": "my-api-key-header",
"in": "header"
}
}
},
"routes": {
"routesDir": "dist",
"middlewareTemplate": "./node_modules/@squidcloud/local-backend/dist/local-backend/openapi-template.hbs"
}
}
Endpoint への security の追加
@Security decorator を使用すると、生成された spec 内で endpoint に authentication が必要であることをマークできます。class level(すべての endpoint)または method level で適用します。
import { SquidService } from '@squidcloud/backend';
import { Get, Route, Security } from 'tsoa';
@Route('secure')
@Security('apiKeyAuth')
export class SecureService extends SquidService {
@Get('data')
async getData(): Promise<string> {
return this.createOpenApiResponse('Secure data');
}
}
@Security decorator は、OpenAPI spec 内で requirement を document 化するだけです。credential を検証する validation logic は、引き続き method 内に実装する必要があります。
Runtime authentication
request context または組み込み auth method を使用して、runtime で credential を検証します。
import { SquidService } from '@squidcloud/backend';
import { Get, Route } from 'tsoa';
@Route('protected')
export class ProtectedService extends SquidService {
@Get('with-api-key')
async withApiKey(): Promise<string> {
const apiKey = this.context.headers?.['x-api-key'];
if (!apiKey || !Object.values(this.apiKeys).includes(apiKey)) {
this.throwOpenApiResponse({ body: 'UNAUTHORIZED', statusCode: 401 });
}
return this.createOpenApiResponse('Authenticated data');
}
@Get('with-bearer')
async withBearer(): Promise<string> {
// Use built-in Squid auth (requires Squid auth integration)
this.assertIsAuthenticated();
const user = this.getUserAuth();
return this.createOpenApiResponse(`Hello, ${user?.userId}`);
}
}
authentication method の詳細については、backend での auth の使用を参照してください。
Error Handling
Error response の返却
適切な status code とともに createOpenApiResponse() を使用します。
@Get('{itemId}')
async getItem(@Path() itemId: string): Promise<Item> {
if (!isValidId(itemId)) {
return this.createOpenApiResponse({ error: `Invalid id: ${itemId}` }, 400);
}
const item = await this.findItem(itemId);
if (!item) {
return this.createOpenApiResponse({ error: 'Item not found' }, 404);
}
return this.createOpenApiResponse(item);
}
throwOpenApiResponse() による Execution の中断
early exit(例: auth failure)では、throwOpenApiResponse() を使用して processing を直ちに停止します。
@Post('transfer')
async transfer(@Body() data: TransferRequest): Promise<object> {
if (!this.isAuthenticated()) {
this.throwOpenApiResponse({ body: 'UNAUTHORIZED', statusCode: 401 });
}
// This code only runs if authenticated
const result = await this.processTransfer(data);
return this.createOpenApiResponse(result);
}
Unhandled exception
method が unhandled error を throw すると、Squid は error message を body とする 500 response を返します。
@Get('risky')
async riskyEndpoint(): Promise<string> {
throw new Error('Something went wrong');
// Returns: 500 with body "Something went wrong"
}
一般的な Error
| Error | 原因 | 解決策 |
|---|---|---|
404 OPENAPI_CONTROLLER_NOT_FOUND | Route path がどの @Route にも一致しない | URL が @Route および method path と一致することを確認する |
| error message を伴う 500 | method 内の unhandled exception | try/catch と createOpenApiResponse() による error handling を追加する |
| Parameter の欠落 | request で required parameter が指定されていない | query/path/body parameter が decorator の想定と一致していることを確認する |
Best Practices
Security
@Securityは spec 内で requirement を document 化するだけなので、常に runtime で credential を検証します- auth failure には
throwOpenApiResponse()を使用して、以降の execution を防ぎます - 処理前に type と size を確認して、file upload を検証します
API design
- 説明的な route path を使用します(例:
@Route('u')ではなく@Route('users')) - 適切な status code を返します(作成は 201、削除は 204、不正な input は 400)
@Tagsを使用して生成された spec 内の endpoint を整理します- 200 以外の status code には、
@Responsedecorator で response を document 化します
Performance
- 必要な data のみを返し、response payload を小さく保ちます
- 不要な processing を避けるため、input を早期に検証します
- JSON 以外の response には、
@Producesを使用して正しい content type を設定します
コード例
CRUD API
import { SquidService } from '@squidcloud/backend';
import { Body, Delete, Get, Patch, Path, Post, Query, Response, Route, Tags } from 'tsoa';
interface Product {
id: string;
name: string;
price: number;
}
@Route('products')
@Tags('Products')
export class ProductService extends SquidService {
@Get()
@Response(200, 'List of products')
async listProducts(@Query() category?: string): Promise<Product[]> {
const products = this.squid.collection<Product>('products');
let query = products.query();
if (category) {
query = query.where('category', '==', category);
}
const results = await query.snapshot();
return this.createOpenApiResponse(results);
}
@Get('{productId}')
@Response(404, 'Product not found')
async getProduct(@Path() productId: string): Promise<Product> {
const ref = this.squid.collection<Product>('products').doc(productId);
const product = await ref.snapshot();
if (!product) {
return this.createOpenApiResponse({ error: 'Product not found' }, 404);
}
return this.createOpenApiResponse(product);
}
@Post()
@Response(201, 'Product created')
async createProduct(@Body() data: Omit<Product, 'id'>): Promise<Product> {
const id = crypto.randomUUID();
const product = { id, ...data };
await this.squid.collection<Product>('products').doc(id).insert(product);
return this.createOpenApiResponse(product, 201);
}
@Patch('{productId}')
async updateProduct(@Path() productId: string, @Body() data: Partial<Product>): Promise<Product> {
const ref = this.squid.collection<Product>('products').doc(productId);
await ref.update(data);
const updated = await ref.snapshot();
return this.createOpenApiResponse(updated);
}
@Delete('{productId}')
@Response(204, 'Product deleted')
async deleteProduct(@Path() productId: string): Promise<void> {
await this.squid.collection<Product>('products').doc(productId).delete();
return this.createOpenApiResponse(undefined, 204);
}
}
API key で保護された endpoint
import { SquidService } from '@squidcloud/backend';
import { Body, Post, Route, Security } from 'tsoa';
@Route('webhooks')
@Security('apiKeyAuth')
export class WebhookReceiverService extends SquidService {
@Post('ingest')
async ingestData(@Body() payload: Record<string, unknown>): Promise<object> {
// Validate API key at runtime
const apiKey = this.context.headers?.['x-api-key'];
if (!apiKey || !Object.values(this.apiKeys).includes(apiKey)) {
this.throwOpenApiResponse({ body: 'UNAUTHORIZED', statusCode: 401 });
}
// Process the payload
await this.squid.collection('events').doc(crypto.randomUUID()).insert({
payload,
receivedAt: new Date().toISOString(),
});
return this.createOpenApiResponse({ status: 'accepted' }, 201);
}
}
External API proxy
import { SquidService } from '@squidcloud/backend';
import { Get, Query, Route } from 'tsoa';
@Route('weather')
export class WeatherProxyService extends SquidService {
@Get('current')
async getCurrentWeather(@Query() city: string): Promise<object> {
const apiKey = this.secrets['WEATHER_API_KEY'] as string;
const response = await fetch(`https://api.weather.example.com/v1/current?city=${encodeURIComponent(city)}`, { headers: { 'X-API-Key': apiKey } });
if (!response.ok) {
return this.createOpenApiResponse({ error: `Weather API returned ${response.status}` }, response.status);
}
const data = await response.json();
return this.createOpenApiResponse(data);
}
}
関連項目
- Executables - Squid Client SDK から backend function を呼び出す
- Webhooks - external service から HTTP callback を受信する
- Rate and quota limiting - endpoint を保護する
- Authentication - backend を保護する
- tsoa documentation - 完全な decorator reference