ããã¯ããªã«ãããããŠæžãããã®ïŒ
APIãèšè¿°ããããã®èšèªãšããŠãTypeSpecãšãããã®ããããããªã®ã§1床ã©ããªãã®ãææ¡ïŒè©ŠããŠãããããªãšããããšã§ã
TypeSpec
TypeSpecã®Webãµã€ãã¯ãã¡ãã§ãã
GitHubãªããžããªãŒã
ããã¥ã¡ã³ãã¯ãã¡ãã
ãªã®ã§ãããæŠèŠãæŽãã«ã¯Microsoft Learnã«ããããã¥ã¡ã³ãããŸãèªãã æ¹ããããããããŸããã
TypeSpec の概要 - TypeSpec とは - TypeSpec | Microsoft Learn
TypeSpecã¯ãMicrosoftãéçºããŠããAPIãèšè¿°ããããã®èšèªã§ãã.tspãšããæ¡åŒµåã®ãã¡ã€ã«ãäœæããããã§ãã
ããTypeSpecãšOpenAPIããã¥ã¡ã³ãçæã®çµã¿åããã§ååãèŠãããã®ã§ãããããã¯ãããŸã§çæã§ãããã®ã®äžçš®ã®
ããã§ããã

TypeSpecã§API仿§ãèšè¿°ããŠãTypeSpecã³ã³ãã€ã©ãŒããšããã¿ãŒãšåŒã°ãããã®ã䜿çšããçµæãçæããŸãã
çŸæç¹ã§ãšããã¿ãŒã«ã¯ä»¥äžãããããã§ãã
- JSON Schema
- OpenAPI
- ProtobufïŒgRPCïŒïŒpreviewïŒ
- Clients
- ã¯ã©ã€ã¢ã³ãã³ãŒãã®çæ
- JavaScriptãJavaãPythonãC#ã察象ïŒããããpreviewïŒ
- Servers
- ãµãŒããŒãµã€ãã®ã³ãŒãçæ
- ASP.NETãJavaScriptã察象ïŒããããpreviewïŒ

ãªã®ã§TypeSpecã§API仿§ãæžãããšããã¿ãŒã䜿ã£ãŠå¥ã®ä»æ§ãã³ãŒãçæãè¡ãããšãã£ã代ç©ã®ããã§ãã
ãããŠç¹ã«OpenAPIããã¥ã¡ã³ããçæã§ãããšããã«æ³šç®ãããããã®ããªãšã
èšèªä»æ§ãšããŠã¯ãã¡ãã
ãŠãŒã¹ã±ãŒã¹ãšããŠã¯ãOpenAPIããã¥ã¡ã³ãã®çæãããªããŒã·ã§ã³ã®å®çŸ©ã
ã·ã³ã¿ãã¯ã¹ãã€ã©ã€ããã³ãŒãè£å®ãªã©ã®ããŒã«ã®ãµããŒããæããããŠããŸãã
ã¡ãªã¿ã«ãOpenAPIããã¥ã¡ã³ãããTypeSpecãžå€æããããšãã§ããããã§ãã
OpenAPI3 to TypeSpec | TypeSpec
TypeSpecã䜿ãå§ããã«ã¯ãã²ãšãŸãNode.jsãããã°å€§äžå€«ããã§ãã
å®éšçæ±ãã§ã¹ã¿ã³ãã¢ãã³ãã€ããªãŒãããããã§ãããçŸæç¹ã ãšNode.jsã§æ±ã£ãæ¹ãããããã§ããã
ç°å¢
ä»åã®ç°å¢ã¯ãã¡ãã
$ node --version v22.15.0 $ npm --version 10.9.2
TypeSpecã䜿ã£ããããžã§ã¯ãã®äœæ
ã§ã¯ãTypeSpecã䜿ã£ããããžã§ã¯ãã®äœæãè¡ã£ãŠã¿ãŸãã
$ npx --package @typespec/compiler@latest tsp init
ä»åã¯TypeSpec 1.0.0-rc.1ã䜿ãããšã«ãªããŸãã
Need to install the following packages: @typespec/compiler@1.0.0-rc.1 Ok to proceed? (y) y
éåžžã¯ä»¥äžã®ã³ãã³ãã䜿ãããã§ãããã°ããŒãã«ã«ã€ã³ã¹ããŒã«ããããªãã£ãã®ã§ããããŸããâŠã
$ npm install -g @typespec/compiler $ tsp init
ããããã¯ã察話圢åŒã§ã©ã®ãããªãããžã§ã¯ããäœæããããèãããŸãã
ãããžã§ã¯ããã³ãã¬ãŒãã
? Select a project template: (Use arrow keys) ⯠Generic REST API TypeSpec library TypeSpec emitter
ãããžã§ã¯ãåãã³ãã³ããå®è¡ããŠãã芪ãã£ã¬ã¯ããªãŒã®ååããããã©ã«ãã§èšå®ãããããã«ãªããŸãã
? Enter a project name: (project-name)
ã€ãŸããæ°èŠãããžã§ã¯ããæ°èŠãã£ã¬ã¯ããªãŒããäœæããã³ãã³ãã§ã¯ãããŸããã
ãšããã¿ãŒãä»åã¯OpenAPIãéžæã
? What emitters do you want to use?: (Press space to select, a to toggle all, i to invert selection and enter to proceed.) ⯠â OpenAPI 3.1 document [@typespec/openapi3] ⯠C# client [@typespec/http-client-csharp] ⯠Java client [@typespec/http-client-java] ⯠JavaScript client [@typespec/http-client-js] ⯠Python client [@typespec/http-client-python] ⯠C# server stubs [@typespec/http-server-csharp]
äŸåã©ã€ãã©ãªãŒãã€ã³ã¹ããŒã«ãããããã ããã¡ã€ã«ãçæãããŸããã
$ ll åèš 84 drwxrwxr-x 3 xxxxx xxxxx 4096 4æ 27 15:20 ./ drwxrwxr-x 44 xxxxx xxxxx 4096 4æ 27 15:16 ../ -rw-rw-r-- 1 xxxxx xxxxx 102 4æ 27 15:19 .gitignore -rw-rw-r-- 1 xxxxx xxxxx 872 4æ 27 15:19 main.tsp drwxrwxr-x 92 xxxxx xxxxx 4096 4æ 27 15:20 node_modules/ -rw-rw-r-- 1 xxxxx xxxxx 54752 4æ 27 15:20 package-lock.json -rw-rw-r-- 1 xxxxx xxxxx 666 4æ 27 15:19 package.json -rw-rw-r-- 1 xxxxx xxxxx 146 4æ 27 15:19 tspconfig.yaml
äœæããããã¡ã€ã«ãèŠãŠã¿ãŸãããã
package.json
{ "name": "project-name", "version": "0.1.0", "type": "module", "peerDependencies": { "@typespec/compiler": "latest", "@typespec/http": "latest", "@typespec/rest": "latest", "@typespec/openapi": "latest", "@typespec/openapi3": "latest" }, "devDependencies": { "@typespec/compiler": "latest", "@typespec/http": "latest", "@typespec/rest": "latest", "@typespec/openapi": "latest", "@typespec/openapi3": "latest" }, "private": true, "packageManager": "npm@11.3.0+sha512.96eb611483f49c55f7fa74df61b588de9e213f80a256728e6798ddc67176c7b07e4a1cfc7de8922422cbce02543714367037536955221fa451b0c4fefaf20c66" }
åããã±ãŒãžã®latestæå®ã«ã¡ãã£ãšé©ããŸããããã€ã³ã¹ããŒã«æ¹æ³ãããã ã£ããããšããããã§ã¯ãªãã
npm install -g @typespec/compilerã®åŸã«tsp initããŠãåãçµæã«ãªããŸããâŠã
TypeSpecã®èšå®ã
tspconfig.yaml
emit: - "@typespec/openapi3" options: "@typespec/openapi3": emitter-output-dir: "{output-dir}/schema" openapi-versions: - 3.1.0
çæãããAPI仿§ã
main.tsp
import "@typespec/http"; using Http; @service(#{ title: "Widget Service" }) namespace DemoService; model Widget { id: string; weight: int32; color: "red" | "blue"; } model WidgetList { items: Widget[]; } @error model Error { code: int32; message: string; } model AnalyzeResult { id: string; analysis: string; } @route("/widgets") @tag("Widgets") interface Widgets { /** List widgets */ @get list(): WidgetList | Error; /** Read widgets */ @get read(@path id: string): Widget | Error; /** Create a widget */ @post create(@body body: Widget): Widget | Error; /** Update a widget */ @patch update(@path id: string, @body body: Widget): Widget | Error; /** Delete a widget */ @delete delete(@path id: string): void | Error; /** Analyze a widget */ @route("{id}/analyze") @post analyze(@path id: string): AnalyzeResult | Error; }
ã³ã³ãã€ã«ããŠã¿ãŸãããã
$ npx tsp compile .
ãã®ãµã³ãã«ã§ã¯ããã®ãããªOpenAPIããã¥ã¡ã³ããçæãããŸããã
tsp-output/schema/openapi.yaml
openapi: 3.1.0 info: title: Widget Service version: 0.0.0 tags: - name: Widgets paths: /widgets: get: operationId: Widgets_list description: List widgets parameters: [] responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/WidgetList' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets post: operationId: Widgets_create description: Create a widget parameters: [] responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Widget' /widgets/{id}: get: operationId: Widgets_read description: Read widgets parameters: - name: id in: path required: true schema: type: string responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets patch: operationId: Widgets_update description: Update a widget parameters: - name: id in: path required: true schema: type: string responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/Widget' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WidgetUpdate' delete: operationId: Widgets_delete description: Delete a widget parameters: - name: id in: path required: true schema: type: string responses: '204': description: 'There is no content to send for this request, but the headers may be useful. ' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets /widgets/{id}/analyze: post: operationId: Widgets_analyze description: Analyze a widget parameters: - name: id in: path required: true schema: type: string responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/AnalyzeResult' default: description: An unexpected error response. content: application/json: schema: $ref: '#/components/schemas/Error' tags: - Widgets components: schemas: AnalyzeResult: type: object required: - id - analysis properties: id: type: string analysis: type: string Error: type: object required: - code - message properties: code: type: integer format: int32 message: type: string Widget: type: object required: - id - weight - color properties: id: type: string weight: type: integer format: int32 color: type: string enum: - red - blue WidgetList: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/Widget' WidgetUpdate: type: object properties: id: type: string weight: type: integer format: int32 color: type: string enum: - red - blue
ããã§ã¯ããã®main.tspã倿ŽããŠAPIå®çŸ©ãè¡ã£ãŠãããŸãããã
ãšãã£ã¿ãŒã®èšå®
ãã®åã«ããšãã£ã¿ãŒã®èšå®ãè¡ããããšæã£ãã®ã§ããã
æšæºã§ã¯Visual Stuido CodeãšVisual Studioåãã®Extensionãããããã§ãã
Visual Studio Extension | TypeSpec
å人çã«ã¯Emacsã䜿ãããã®ã§ããããã¡ãã®modeã¯ã¡ãã£ãšããŸãçµã¿èŸŒããŸããã§ããâŠã
GitHub - pradyuman/typespec-ts-mode: Emacs major mode for TypeSpec (using tree-sitter)
TypeSpec - LSP Mode - LSP support for Emacs
仿¹ããªãã®ã§typescript-modeã§ããããšã«ããŸããããç¹ã«åé¡ãªãã£ãã§ãã
API仿§ãèšè¿°ããŠãOpenAPIããã¥ã¡ã³ããçæããŠã¿ã
ããã§ã¯API仿§ãèšè¿°ããŠãOpenAPIããã¥ã¡ã³ããçæããŠã¿ãŸãããã
ãé¡ã¯ãå°ãåã«MicroProfile OpenAPI 4.0ãããŒãã«ãããã¡ãã®æžç±ãæ±ãREST APIã®åçŸã«ããããšæããŸãã
WildFly 36 × SmallRye OpenAPI 4.0で出力するOpenAPIドキュメントのバージョンを3.1、3.0に切り替える - CLOVER🍀
äœæããAPI仿§ã¯ãã¡ãã
main.tsp
import "@typespec/http"; import "@typespec/openapi"; using Http; using OpenAPI; @service(#{ title: "My Sample REST API" }) @route("/api") namespace SampleBookService; model BookRequest { @doc("ç»é²ããæžç±ã®ã¿ã€ãã«") @example("Javaã®æ¬") @maxLength(100) title: string; @doc("ç»é²ããæžç±ã®äŸ¡æ Œ") @example(1500) @minValue(1) price: int32; @doc("ç»é²ããæžç±ã®åºçæ¥") @example(plainDate.fromISO("2024-10-13")) publishDate?: plainDate; // optional } model BookResponse { isbn13: string; title: string; price: int32; publishDate?: plainDate; } @route("/books") @tag("book") namespace Books { @summary("ç»é²ãããæžç±ããã¹ãŠè¿åŽãã") @doc("ç»é²ãããæžç±ãäŸ¡æ Œã®éé ã«ãœãŒãããŠãã¹ãŠè¿åŽãã") @operationId("findAllBooks") @get op list(): { @body body: BookResponse[]; }; @summary("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã") @operationId("findBookByIsbn13") @get op read( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, ): BookResponse | NotFoundResponse; @summary("æå®ãããISBNã«æžç±ãç»é²ãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãç»é²ãã") @operationId("registerBook") op create( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, @doc("ç»é²ããæžç±ããŒã¿") @body body: BookRequest, ): { @statusCode _: "201"; @body body: BookResponse; } | { @statusCode _: "400"; @body body: BadRequestResponse; }; @summary("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã") @operationId("deleteBookByIsbn13") @delete op delete( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, ): NoContentResponse; }
äž»ã«åç §ããããã¥ã¡ã³ãã¯ãã¡ãã§ãã
TypeSpec for OpenAPI Developers | TypeSpec
ããã§OpenAPI仿§ã§ã®èšè¿°æ¹æ³ãšãTypeSpecã§ã®èšè¿°æ¹æ³ãåãããŠãããŸãã
æåã«çæãããTypeSpecã®ãµã³ãã«ã®Operationã®å®çŸ©ããããªæãã§ããã
interface Widgets { /** List widgets */ @get list(): WidgetList | Error; /** Read widgets */ @get read(@path id: string): Widget | Error; /** Create a widget */ @post create(@body body: Widget): Widget | Error; /** Update a widget */ @patch update(@path id: string, @body body: Widget): Widget | Error; /** Delete a widget */ @delete delete(@path id: string): void | Error; /** Analyze a widget */ @route("{id}/analyze") @post analyze(@path id: string): AnalyzeResult | Error; }
ã¡ãããšsummaryãdescriptionãæå®ããããoperationIdãæ±ºãããããããšãããšã¡ãã£ãšé·ãã«ãªããŸããã
@route("/books") @tag("book") namespace Books { @summary("ç»é²ãããæžç±ããã¹ãŠè¿åŽãã") @doc("ç»é²ãããæžç±ãäŸ¡æ Œã®éé ã«ãœãŒãããŠãã¹ãŠè¿åŽãã") @operationId("findAllBooks") @get op list(): { @body body: BookResponse[]; }; @summary("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã") @operationId("findBookByIsbn13") @get op read( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, ): BookResponse | NotFoundResponse; @summary("æå®ãããISBNã«æžç±ãç»é²ãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãç»é²ãã") @operationId("registerBook") op create( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, @doc("ç»é²ããæžç±ããŒã¿") @body body: BookRequest, ): { @statusCode _: "201"; @body body: BookResponse; } | { @statusCode _: "400"; @body body: BadRequestResponse; }; @summary("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã") @doc("æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã") @operationId("deleteBookByIsbn13") @delete op delete( @path @minLength(14) @maxLength(14) @doc("ISBN") isbn13: string, ): NoContentResponse; }
@operationIdãªã©ã®OpenAPIçšã®ãã®ã¯ãOpenAPIã®importããã³useã
ããŠãããªããšäœ¿ãããšãã§ããŸããã
import "@typespec/openapi"; ... using OpenAPI;
ããšãã¬ã¹ãã³ã¹ããã£ã«å¯Ÿããdescriptionã¯ã©ãããŠãããŸãä»ããããŸããã§ããâŠãããã¥ã¡ã³ããèŠããš@docã§
ãããããªã®ã§ããâŠã
æ £ããªãå Žåã§ããã€æ¢åã®OpenAPIããã¥ã¡ã³ããããå Žåã¯1床ãã¡ãã§å€æããŠã¿ããšåèã«ãªãã§ããããã
OpenAPI3 to TypeSpec | TypeSpec
ã§ã¯ãèšè¿°ããAPI仿§ããOpenAPIããã¥ã¡ã³ããçæããŠã¿ãŸãã
$ npx tsp compile .
çµæã¯ãã¡ã
tsp-output/schema/openapi.yaml
openapi: 3.1.0 info: title: My Sample REST API version: 0.0.0 tags: - name: book paths: /api/books: get: operationId: findAllBooks summary: ç»é²ãããæžç±ããã¹ãŠè¿åŽãã description: ç»é²ãããæžç±ãäŸ¡æ Œã®éé ã«ãœãŒãããŠãã¹ãŠè¿åŽãã parameters: [] responses: '200': description: The request has succeeded. content: application/json: schema: type: array items: $ref: '#/components/schemas/BookResponse' tags: - book /api/books/{isbn13}: get: operationId: findBookByIsbn13 summary: æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã description: æå®ãããISBNã«å¯Ÿå¿ããæžç±ãååŸãã parameters: - name: isbn13 in: path required: true description: ISBN schema: type: string minLength: 14 maxLength: 14 responses: '200': description: The request has succeeded. content: application/json: schema: $ref: '#/components/schemas/BookResponse' '404': description: The server cannot find the requested resource. tags: - book post: operationId: registerBook summary: æå®ãããISBNã«æžç±ãç»é²ãã description: æå®ãããISBNã«å¯Ÿå¿ããæžç±ãç»é²ãã parameters: - name: isbn13 in: path required: true description: ISBN schema: type: string minLength: 14 maxLength: 14 responses: '201': description: The request has succeeded and a new resource has been created as a result. content: application/json: schema: $ref: '#/components/schemas/BookResponse' '400': description: The server could not understand the request due to invalid syntax. content: application/json: schema: $ref: '#/components/schemas/TypeSpec.Http.BadRequestResponse' tags: - book requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookRequest' description: ç»é²ããæžç±ããŒã¿ delete: operationId: deleteBookByIsbn13 summary: æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã description: æå®ãããISBNã«å¯Ÿå¿ããæžç±ãåé€ãã parameters: - name: isbn13 in: path required: true description: ISBN schema: type: string minLength: 14 maxLength: 14 responses: '204': description: There is no content to send for this request, but the headers may be useful. tags: - book components: schemas: BookRequest: type: object required: - title - price properties: title: type: string maxLength: 100 description: ç»é²ããæžç±ã®ã¿ã€ãã« examples: - Javaã®æ¬ price: type: integer format: int32 minimum: 1 description: ç»é²ããæžç±ã®äŸ¡æ Œ examples: - 1500 publishDate: type: string format: date description: ç»é²ããæžç±ã®åºçæ¥ examples: - '2024-10-13' BookResponse: type: object required: - isbn13 - title - price properties: isbn13: type: string title: type: string price: type: integer format: int32 publishDate: type: string format: date TypeSpec.Http.BadRequestResponse: type: object required: - statusCode properties: statusCode: type: number enum: - 400 description: The status code. description: The server could not understand the request due to invalid syntax.
ã²ãšãŸãããããã§ãã
ã³ãŒããã©ãŒããããè¡ãå Žåã¯ãã¡ãã
$ npx tsp format '**/*.tsp'
ã¹ã¿ã€ã«ã¬ã€ããèŠãŠãããšããã§ãããã
ãããã«
APIãèšè¿°ããããã®èšèªãTypeSpecã䜿ã£ãŠOpenAPIããã¥ã¡ã³ããçæããŠã¿ãŸããã
TypeSpecãªãã§ã¯ã®æžãæ¹ãèŠããªããšãããªãã®ã§ãTypeSpecãæžããŠOpenAPIããã¥ã¡ã³ããçæããŠãæã£ããšããã«
ãªã£ãŠãããïŒãšãã£ã確èªãç¹°ãè¿ãããšã«ãªããŸããã
èšè¿°æ¹æ³ãå€ãã£ãã®ã§ããããããã®ã§ãããã
å人çã«ã¯OpenAPIã®ã¿ã«çµã£ãŠèŠããšãMicroProfile OpenAPIã®ãããªã³ãŒãããOpenAPIããã¥ã¡ã³ããçæããã®ãš
ããŸãå€ãããªãæ°ãããã®ã§ããããã®ãããªãã¬ãŒã ã¯ãŒã¯èªèº«ããµããŒãããŠããªãå Žåããä»ã®ãšããã¿ãŒãå©çšã§ããã
ããã®ã§API仿§ã®ç®¡çãTypeSpecã«ãŸãšããŠãããšè¯ããåºãã®ããªãšæããŸãã
ã²ãšãŸãTypeSpecãã©ããããã®ãã¯æŒãããããã®ã§ãããšããŸãããã