@nxgt/typespec 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,10 @@ Each shape here is one the `@nxgt/*` packages already send on the wire.
8
8
 
9
9
  ## Install
10
10
 
11
+ A new project needs no install of its own: `tsp init` with the package's
12
+ template installs everything ([Start a project](#start-a-project)). For an
13
+ existing project, install the library and its peers:
14
+
11
15
  ```sh
12
16
  bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @typespec/openapi3 @nxgt/openapi-codegen
13
17
  ```
@@ -16,6 +20,8 @@ bun add -d @nxgt/typespec @typespec/compiler @typespec/http @typespec/openapi @t
16
20
  are peer dependencies,
17
21
  and `typescript` 6 too, as for every `@nxgt` package. `@typespec/openapi3`
18
22
  compiles the spec, and `@nxgt/openapi-codegen` generates the code from it.
23
+ The generator's CLI, `nxgt-openapi`, runs on [Bun](https://bun.sh): it needs
24
+ `bun` on the PATH, even when called through `npx` or an npm script.
19
25
  Emit OpenAPI 3.1 or 3.2 in `tspconfig.yaml`: `@typespec/openapi3` emits 3.0
20
26
  by default, and the generator refuses it. Every convention here is compiled
21
27
  to both in CI, and generates the same code from either.
@@ -33,6 +39,35 @@ options:
33
39
 
34
40
  ## Usage
35
41
 
42
+ ### Start a project
43
+
44
+ `tsp init` with the package's template starts an API project with these
45
+ conventions in place. Run it in an empty directory, with
46
+ [Bun](https://bun.sh) on the PATH for the generator:
47
+
48
+ ```sh
49
+ npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json \
50
+ --template nxgt -y --project-name petstore
51
+ npx tsp compile .
52
+ npx nxgt-openapi generate
53
+ ```
54
+
55
+ It installs the compiler, `@typespec/http`, `@typespec/openapi`,
56
+ `@typespec/openapi3`, this package and `@nxgt/openapi-codegen`, and writes:
57
+
58
+ - `tspconfig.yaml`: the linter's recommended rules, and OpenAPI 3.1 written
59
+ to `openapi/openapi.yaml`;
60
+ - `main.tsp`: the service in the namespace `Api`, with `@operationIds`, and a
61
+ `Users` resource with a paged, sorted list, `get`, `create`, `update` and
62
+ `delete`, and their error aliases;
63
+ - `openapi-codegen.config.ts`: the generator's input, and `src/generated`
64
+ with the Hono routes;
65
+ - `README.md`: the `api` and `api:check` scripts to add to `package.json`.
66
+
67
+ Drop `-y` and its options to be asked instead. The next steps, serving the
68
+ routes and renaming the resource, are in
69
+ [Getting started](docs/guide/getting-started.md).
70
+
36
71
  ### A spec split across files
37
72
 
38
73
  A spec that grows is split by role: one file per resource's models, one per
@@ -513,6 +548,9 @@ envelope and the rate limit's headers.
513
548
 
514
549
  ## Traps
515
550
 
551
+ - **`nxgt-openapi` fails with `env: 'bun': No such file or directory`**
552
+ without Bun on the PATH: install it, `curl -fsSL https://bun.sh/install | bash`
553
+ ([troubleshooting](docs/troubleshooting.md#env-bun-no-such-file-or-directory)).
516
554
  - **Do not declare a schema named `ValidationErrorBody`.** The generator
517
555
  declares it itself, and two schemas of that name fail with
518
556
  `name_collision`. `BadRequestBody` carries the `issues` instead.
@@ -570,6 +608,8 @@ envelope and the rate limit's headers.
570
608
 
571
609
  ## Documentation
572
610
 
611
+ - [Getting started](docs/guide/getting-started.md): the `tsp init`
612
+ template, the files it writes, and the steps from the spec to a Hono route;
573
613
  - [Error replies](docs/guide/errors.md): the envelope, each response, the
574
614
  aliases by verb, and what the generator makes of them;
575
615
  - [Pagination](docs/guide/pagination.md): offset and cursor pages, and
package/docs/README.md CHANGED
@@ -4,6 +4,7 @@ The [package README](../README.md) is the short version.
4
4
 
5
5
  | Page | Read it when |
6
6
  | --- | --- |
7
+ | [Getting started](guide/getting-started.md) | starting a new API project from the `tsp init` template, up to a Hono route |
7
8
  | [Error replies](guide/errors.md) | declaring what an operation answers when it fails |
8
9
  | [Pagination](guide/pagination.md) | listing a collection a page at a time |
9
10
  | [Authentication](guide/auth.md) | protecting an operation with a `@nxgt/janus` session |
@@ -162,7 +162,9 @@ its own way, through `onValidationError`, turns that off with the generator's
162
162
  | `Forbidden` | 403 | `ForbiddenBody` | authenticated, but not allowed; a `@nxgt/janus-hono` guard sends `AccessDenied` instead, and `janusErrors()` its own `{ code }` body ([Authentication](auth.md)) |
163
163
  | `NotFound` | 404 | `NotFoundBody` | nothing is there |
164
164
  | `Conflict` | 409 | `ConflictBody` | the request conflicts with the resource's state, such as a stale `version` |
165
+ | `IdempotencyInProgress` | 409 | `ConflictBody` | the first request with this `Idempotency-Key` still runs; with the `RateLimit-*` headers and `Retry-After` ([Headers](headers.md)) |
165
166
  | `UnprocessableEntity` | 422 | `UnprocessableEntityBody` | well-formed, but not something the server can act on |
167
+ | `IdempotencyKeyReused` | 422 | `UnprocessableEntityBody` | an `Idempotency-Key` sent again with another body; with the `RateLimit-*` headers ([Headers](headers.md)) |
166
168
  | `TooManyRequests` | 429 | `TooManyRequestsBody` | too many requests; with the `RateLimit-*` headers and `Retry-After` ([Headers](headers.md)) |
167
169
  | `InternalServerError` | 500 | `InternalServerErrorBody` | the server failed; with `validateResponses`, `@nxgt/openapi-hono` sends it with `errors.response-validation-failed` when a reply breaks the spec |
168
170
  | `ErrorResponse<Status>` | any | `ErrorBody<Status>` | any other status |
@@ -0,0 +1,277 @@
1
+ # Getting started
2
+
3
+ This page starts a new API project from the package's `tsp init` template,
4
+ then takes it from the spec to a running Hono route.
5
+
6
+ ## Start a project
7
+
8
+ You need Node.js for `tsp`, and [Bun](https://bun.sh) on the PATH for the
9
+ generator, `nxgt-openapi`, which runs on Bun
10
+ ([Compile and generate](#compile-and-generate)).
11
+
12
+ Run `tsp init` with the template, in an empty directory:
13
+
14
+ ```sh
15
+ mkdir petstore && cd petstore
16
+ npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
17
+ ```
18
+
19
+ `tsp init` asks for the template, the project's name and the emitters. To
20
+ answer them on the command line instead:
21
+
22
+ ```sh
23
+ npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json \
24
+ --template nxgt -y --project-name petstore
25
+ ```
26
+
27
+ `--package=@typespec/compiler` matters: in a directory without the compiler,
28
+ a bare `npx tsp` runs `tsp`, an unrelated npm package. `bunx
29
+ --package=@typespec/compiler tsp init …` works too.
30
+
31
+ `tsp init` writes into the current directory, then runs `npm install`, so it
32
+ needs the network. The template ships from 0.9.0 on; an older version, or no
33
+ network, fails before anything is written
34
+ ([troubleshooting](../troubleshooting.md#init-template-invalid-json-unable-to-parse-scaffoldingjson-unexpected-token--is-not-valid-json)).
35
+
36
+ ## What it writes
37
+
38
+ | File | What it holds |
39
+ | --- | --- |
40
+ | `package.json` | the compiler, `@typespec/http`, `@typespec/openapi`, `@typespec/openapi3`, `@nxgt/typespec` and `@nxgt/openapi-codegen`, at their latest versions |
41
+ | `tspconfig.yaml` | the linter's recommended rules, and OpenAPI 3.1 written to `openapi/openapi.yaml` |
42
+ | `main.tsp` | the service, marked with `@operationIds`, and a `Users` resource |
43
+ | `openapi-codegen.config.ts` | the generator's input and output, with the Hono routes on |
44
+ | `README.md` | the commands below, and the scripts to add |
45
+ | `.gitignore` | `node_modules/`, and TypeSpec's default output |
46
+
47
+ `tspconfig.yaml` turns the linter on and emits OpenAPI 3.1, which
48
+ `@nxgt/openapi-codegen` reads (it refuses 3.0, the emitter's default):
49
+
50
+ ```yaml
51
+ # tspconfig.yaml
52
+ linter:
53
+ extends:
54
+ - "@nxgt/typespec/recommended"
55
+ emit:
56
+ - "@typespec/openapi3"
57
+ options:
58
+ "@typespec/openapi3":
59
+ emitter-output-dir: "{project-root}/openapi"
60
+ openapi-versions:
61
+ - 3.1.0
62
+ output-file: openapi.yaml
63
+ ```
64
+
65
+ `main.tsp` declares the service in the namespace `Api`, titled after the
66
+ project, and one resource with each convention in place: a paged, filtered and sorted
67
+ list, and the error aliases of each verb.
68
+
69
+ ```tsp
70
+ // main.tsp (excerpt)
71
+ @service(#{ title: "petstore" })
72
+ @operationIds
73
+ namespace Api;
74
+
75
+ model User {
76
+ @visibility(Lifecycle.Read)
77
+ id: uuid;
78
+
79
+ @minLength(1)
80
+ @maxLength(80)
81
+ name: string;
82
+
83
+ email: email;
84
+
85
+ ...Timestamps;
86
+ }
87
+
88
+ @route("/users")
89
+ @tag("Users")
90
+ interface Users {
91
+ @get
92
+ list(
93
+ ...UserFilters,
94
+ ...PageParameters,
95
+ ...SortParameters<"name" | "createdAt">,
96
+ ): Page<User> | ListErrors;
97
+
98
+ @get
99
+ get(@path id: uuid): User | GetErrors;
100
+
101
+ @post
102
+ create(@body user: Create<User>): {
103
+ @statusCode _: 201;
104
+ @body user: User;
105
+ } | CreateErrors;
106
+
107
+ @patch
108
+ update(@path id: uuid, @body user: MergePatchUpdate<User>): User | UpdateErrors;
109
+
110
+ @delete
111
+ delete(@path id: uuid): NoContentResponse | DeleteErrors;
112
+ }
113
+ ```
114
+
115
+ `@operationIds` names the operations `listUsers`, `getUser`, `createUser`,
116
+ `updateUser` and `deleteUser` ([Operation ids](operation-ids.md)).
117
+
118
+ ```ts
119
+ // openapi-codegen.config.ts
120
+ import { defineConfig } from '@nxgt/openapi-codegen';
121
+
122
+ export default defineConfig({
123
+ input: 'openapi/openapi.yaml',
124
+ output: 'src/generated',
125
+ hono: true,
126
+ });
127
+ ```
128
+
129
+ ## Compile and generate
130
+
131
+ Compile the spec to `openapi/openapi.yaml`, then generate the code from it.
132
+ `nxgt-openapi` runs on Bun: it needs `bun` on the PATH, even through `npx` or
133
+ an npm script, and fails with `env: 'bun': No such file or directory`
134
+ without it ([troubleshooting](../troubleshooting.md#env-bun-no-such-file-or-directory)).
135
+ `bunx nxgt-openapi generate` does the same as the `npx` line.
136
+
137
+ ```sh
138
+ npx tsp compile .
139
+ npx nxgt-openapi generate
140
+ ```
141
+
142
+ ```text
143
+ openapi/openapi.yaml → src/generated: 5 written, 0 unchanged
144
+ ```
145
+
146
+ `src/generated` then holds `types.ts`, `zod.ts`, `operations.ts`, `paths.ts`
147
+ and `hono.ts`. The linter runs in `tsp compile`; the template's spec passes
148
+ it.
149
+
150
+ ## Add the scripts
151
+
152
+ `tsp init` writes its own `package.json`, so the template cannot add
153
+ scripts. Add these two, so the code never lags the spec:
154
+
155
+ ```json
156
+ {
157
+ "scripts": {
158
+ "api": "tsp compile . && nxgt-openapi generate",
159
+ "api:check": "tsp compile . && nxgt-openapi generate --check"
160
+ }
161
+ }
162
+ ```
163
+
164
+ `api:check` exits with 1 and lists the files when the generated code is out
165
+ of date: run it in CI.
166
+
167
+ ## Serve it
168
+
169
+ The generated `hono.ts` binds the routes to the spec through
170
+ [`@nxgt/openapi-hono`](https://www.npmjs.com/package/@nxgt/openapi-hono).
171
+ Install it, Hono, and zod, which the generated validators import, with the
172
+ package manager `tsp init` used:
173
+
174
+ ```sh
175
+ npm install @nxgt/openapi-hono hono zod
176
+ ```
177
+
178
+ Then register a handler per operation. Each one reads its validated input,
179
+ and may only answer the replies the spec declares:
180
+
181
+ ```ts
182
+ // src/app.ts
183
+ import { Hono } from 'hono';
184
+ import { createRoutes } from './generated/hono';
185
+ import type { NotFoundBody } from './generated/types';
186
+
187
+ const app = new Hono();
188
+ const routes = createRoutes(app);
189
+
190
+ routes.operation('getUser', async (c) => {
191
+ const { id } = c.req.valid('param');
192
+ const user = await users.find(id);
193
+ if (!user) {
194
+ const body: NotFoundBody = {
195
+ status: 404,
196
+ message: 'errors.not-found',
197
+ timestamp: new Date().toISOString(),
198
+ };
199
+ return c.json(body, 404);
200
+ }
201
+ return c.json(user, 200);
202
+ });
203
+
204
+ routes.operation('listUsers', async (c) => {
205
+ const { name, page = 1, pageSize = 20, orderBy, direction } = c.req.valid('query');
206
+ return c.json(await users.page({ name, page, pageSize, orderBy, direction }), 200);
207
+ });
208
+
209
+ export default app;
210
+ ```
211
+
212
+ `users` stands for your data layer. Annotate an error body with its generated
213
+ type, as `NotFoundBody` above: an object literal passed straight to
214
+ `c.json(…, 404)` is inferred beside the handler's other returns, its fields
215
+ turn optional, and the handler fails with `TS2769: No overload matches this
216
+ call`. A `418`,
217
+ or a 404 without `timestamp`, does not compile either. More on the routes in the
218
+ [`@nxgt/openapi-hono` README](https://www.npmjs.com/package/@nxgt/openapi-hono),
219
+ and on serving a page in [Pagination](pagination.md#serving-it).
220
+
221
+ ## Rename the resource
222
+
223
+ `Users` is an example. To make it your own resource, rename the model, its
224
+ filters, the interface, the route and the tag in `main.tsp`, and the body
225
+ parameters with them:
226
+
227
+ ```tsp
228
+ model Pet {
229
+ @visibility(Lifecycle.Read)
230
+ id: uuid;
231
+
232
+ @minLength(1)
233
+ name: string;
234
+
235
+ ...Timestamps;
236
+ }
237
+
238
+ model PetFilters {
239
+ @query
240
+ name?: string;
241
+ }
242
+
243
+ @route("/pets")
244
+ @tag("Pets")
245
+ interface Pets {
246
+ @get
247
+ list(...PetFilters, ...PageParameters, ...SortParameters<"name" | "createdAt">): Page<Pet> | ListErrors;
248
+
249
+ @get
250
+ get(@path id: uuid): Pet | GetErrors;
251
+
252
+ @post
253
+ create(@body pet: Create<Pet>): {
254
+ @statusCode _: 201;
255
+ @body pet: Pet;
256
+ } | CreateErrors;
257
+
258
+ @patch
259
+ update(@path id: uuid, @body pet: MergePatchUpdate<Pet>): Pet | UpdateErrors;
260
+
261
+ @delete
262
+ delete(@path id: uuid): NoContentResponse | DeleteErrors;
263
+ }
264
+ ```
265
+
266
+ The operation names stay; `@operationIds` takes the resource from the
267
+ interface, so the ids become `listPets`, `getPet`, `createPet`, `updatePet`
268
+ and `deletePet`. Run `tsp compile .` and `nxgt-openapi generate` again, and
269
+ the generated types, `Pet` and `PetPage`, follow. A name the singular rule
270
+ does not know takes `@operationIds(#{ singular: "…" })` on the interface
271
+ ([Operation ids](operation-ids.md)).
272
+
273
+ A second resource is a second model and interface, in the same namespace or
274
+ in files of their own ([README](../README.md#a-spec-split-across-files)).
275
+
276
+ Next: what each error alias holds, in [Error replies](errors.md), and the
277
+ rules the linter checks, in [Linter](linter.md).
package/docs/roadmap.md CHANGED
@@ -6,12 +6,7 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
6
6
 
7
7
  ## Now
8
8
 
9
- - **A linter.** The ruleset `@nxgt/typespec/recommended`, which a spec
10
- extends in `tspconfig.yaml`, warns of a list that returns an array
11
- (`list-returns-page`), a service without `@operationIds`
12
- (`service-operation-ids`), and an error reply without the envelope
13
- (`error-body-shape`). Each rule can be turned off, for the spec or for one
14
- operation.
9
+ Nothing in progress.
15
10
 
16
11
  ## Later
17
12
 
@@ -25,6 +20,22 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
25
20
 
26
21
  ## Shipped
27
22
 
23
+ ### 0.9.0
24
+
25
+ - **A `tsp init` template.** `tsp init` with the package's template
26
+ starts a project with the conventions in place: `@operationIds`, a paged
27
+ resource with its error aliases, the linter on, and the OpenAPI that
28
+ `@nxgt/openapi-codegen` generates from.
29
+
30
+ ### 0.8.0
31
+
32
+ - **A linter.** The ruleset `@nxgt/typespec/recommended`, which a spec
33
+ extends in `tspconfig.yaml`, warns of a list that returns an array
34
+ (`list-returns-page`), a service without `@operationIds`
35
+ (`service-operation-ids`), and an error reply without the envelope
36
+ (`error-body-shape`). Each rule can be turned off, for the spec or for one
37
+ operation.
38
+
28
39
  ### 0.7.0
29
40
 
30
41
  - **Error aliases by verb.** `ListErrors`, `GetErrors`, `CreateErrors`,
@@ -27,6 +27,67 @@ no `package.json` to resolve it from.
27
27
  devDependencies, with the command in the
28
28
  [README's Install section](../README.md#install).
29
29
 
30
+ ## `init-template-invalid-json: Unable to parse …/scaffolding.json: Unexpected token … is not valid JSON`
31
+
32
+ **When:** `tsp init` with the package's template, before it writes anything.
33
+
34
+ ```text
35
+ error init-template-invalid-json: Unable to parse https://unpkg.com/@nxgt/typespec@0.8.0/templates/scaffolding.json: Unexpected token 'N', "Not found:"... is not valid JSON. Check that the template URL is correct.
36
+ ```
37
+
38
+ **Why:** the URL names no template, so unpkg answers a page of text, `Not
39
+ found: …` or `Package version not found: …`, which `tsp init` cannot parse.
40
+ The template ships from 0.9.0 on: a version before it in the URL, a version
41
+ that is not published, or a typo in the path all answer that page.
42
+
43
+ **Fix:** drop the version, or name 0.9.0 or later, and keep the path as it is:
44
+
45
+ ```sh
46
+ npm view @nxgt/typespec version # 0.9.0 or later
47
+ npx --package=@typespec/compiler tsp init https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
48
+ ```
49
+
50
+ ## `init-template-download-failed: Failed to download template from …: fetch failed`
51
+
52
+ **When:** `tsp init` with the package's template, before it writes anything.
53
+
54
+ ```text
55
+ error init-template-download-failed: Failed to download template from https://unpkg.com/@nxgt/typespec/templates/scaffolding.json: fetch failed. Check that the template URL is correct.
56
+ ```
57
+
58
+ **Why:** `tsp init` could not reach the host: no network, a firewall or
59
+ proxy in the way, or a host name mistyped. Nothing answered, unlike the entry
60
+ above, where unpkg answers a page that is not the template.
61
+
62
+ **Fix:** check that the host answers from that machine, then run `tsp init`
63
+ again:
64
+
65
+ ```sh
66
+ curl -fsSI https://unpkg.com/@nxgt/typespec/templates/scaffolding.json
67
+ ```
68
+
69
+ ## `env: 'bun': No such file or directory`
70
+
71
+ **When:** `nxgt-openapi generate` runs, through `npx`, an npm script such as
72
+ the template's `api`, or a CI step, on a machine without Bun. A shell in
73
+ another locale quotes it `‘bun’`; macOS prints `env: bun: No such file or
74
+ directory`. The exit code is 127.
75
+
76
+ **Why:** `@nxgt/openapi-codegen`'s CLI, `nxgt-openapi`, runs on
77
+ [Bun](https://bun.sh): its first line is `#!/usr/bin/env bun`. `npx` and
78
+ `npm run` find the script, then the system looks for `bun` to run it.
79
+ `tsp compile` runs on Node and is not affected.
80
+
81
+ **Fix:** install Bun, and make sure `bun` is on the PATH of the shell or the
82
+ CI job that runs the generator:
83
+
84
+ ```sh
85
+ curl -fsSL https://bun.sh/install | bash
86
+ bunx nxgt-openapi generate
87
+ ```
88
+
89
+ In GitHub Actions, add `oven-sh/setup-bun@v2` before the step.
90
+
30
91
  ## `Duplicate type name: 'NotFoundBody'`
31
92
 
32
93
  **When:** `tsp compile` runs on a spec that declares its own model named like
package/lib/errors.tsp CHANGED
@@ -130,6 +130,29 @@ model TooManyRequests {
130
130
  @body body: TooManyRequestsBody;
131
131
  }
132
132
 
133
+ /**
134
+ * A request with this `Idempotency-Key` is still running. Retry it as it is,
135
+ * after `Retry-After`.
136
+ */
137
+ @error
138
+ model IdempotencyInProgress {
139
+ @statusCode _: 409;
140
+ ...RateLimitHeaders;
141
+ ...RetryAfterHeader;
142
+ @body body: ConflictBody;
143
+ }
144
+
145
+ /**
146
+ * This `Idempotency-Key` was already used for a request with another body. A
147
+ * new request takes a new key.
148
+ */
149
+ @error
150
+ model IdempotencyKeyReused {
151
+ @statusCode _: 422;
152
+ ...RateLimitHeaders;
153
+ @body body: UnprocessableEntityBody;
154
+ }
155
+
133
156
  /**
134
157
  * The server failed. With `validateResponses`, `@nxgt/openapi-hono` answers
135
158
  * it too when a reply breaks the spec, with `errors.response-validation-failed`.
package/lib/headers.tsp CHANGED
@@ -51,26 +51,3 @@ model RetryAfterHeader {
51
51
  @header("Retry-After")
52
52
  retryAfter?: integer;
53
53
  }
54
-
55
- /**
56
- * A request with this `Idempotency-Key` is still running. Retry it as it is,
57
- * after `Retry-After`.
58
- */
59
- @error
60
- model IdempotencyInProgress {
61
- @statusCode _: 409;
62
- ...RateLimitHeaders;
63
- ...RetryAfterHeader;
64
- @body body: ConflictBody;
65
- }
66
-
67
- /**
68
- * This `Idempotency-Key` was already used for a request with another body. A
69
- * new request takes a new key.
70
- */
71
- @error
72
- model IdempotencyKeyReused {
73
- @statusCode _: 422;
74
- ...RateLimitHeaders;
75
- @body body: UnprocessableEntityBody;
76
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/typespec",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "lib",
12
+ "templates",
12
13
  "docs",
13
14
  "README.md",
14
15
  "package.json",
@@ -0,0 +1,33 @@
1
+ # {{name}}
2
+
3
+ The API is written in TypeSpec, in `main.tsp`, with the
4
+ [`@nxgt/typespec`](https://www.npmjs.com/package/@nxgt/typespec) conventions.
5
+
6
+ Compile it to `openapi/openapi.yaml`, then generate the types, validators
7
+ and routes into `src/generated`. The generator, `nxgt-openapi`, runs on
8
+ [Bun](https://bun.sh): it needs `bun` on the PATH, even through `npx` or an
9
+ npm script.
10
+
11
+ ```sh
12
+ npx tsp compile .
13
+ npx nxgt-openapi generate
14
+ ```
15
+
16
+ Chain both in `package.json`, so the code never lags the spec:
17
+
18
+ ```json
19
+ {
20
+ "scripts": {
21
+ "api": "tsp compile . && nxgt-openapi generate",
22
+ "api:check": "tsp compile . && nxgt-openapi generate --check"
23
+ }
24
+ }
25
+ ```
26
+
27
+ `tspconfig.yaml` turns on the linter's recommended rules, which warn of a
28
+ list without a page, a service without `@operationIds`, and an error reply
29
+ without the envelope.
30
+
31
+ The next steps, serving the routes with Hono and renaming the `Users`
32
+ resource, are in
33
+ [Getting started](https://github.com/softistx/nxgt-http/blob/develop/packages/typespec/docs/guide/getting-started.md).
@@ -0,0 +1,68 @@
1
+ // The API's spec. `tsp compile .` writes openapi/openapi.yaml, and
2
+ // `nxgt-openapi generate` the types, validators and routes from it.
3
+ import "@typespec/http";
4
+ import "@nxgt/typespec";
5
+
6
+ using Http;
7
+ using Nxgt;
8
+
9
+ /** The {{name}} API. */
10
+ @service(#{ title: "{{name}}" })
11
+ @operationIds
12
+ namespace Api;
13
+
14
+ /** Someone who uses the API. */
15
+ model User {
16
+ @visibility(Lifecycle.Read)
17
+ id: uuid;
18
+
19
+ @minLength(1)
20
+ @maxLength(80)
21
+ name: string;
22
+
23
+ email: email;
24
+
25
+ ...Timestamps;
26
+ }
27
+
28
+ /** What the user list filters on. */
29
+ model UserFilters {
30
+ /** Users of this name. */
31
+ @query
32
+ name?: string;
33
+ }
34
+
35
+ // Verbs @operationIds completes: list is listUsers, get is getUser.
36
+ @route("/users")
37
+ @tag("Users")
38
+ interface Users {
39
+ /** One page of the users, filtered and sorted. */
40
+ @get
41
+ list(
42
+ ...UserFilters,
43
+ ...PageParameters,
44
+ ...SortParameters<"name" | "createdAt">,
45
+ ): Page<User> | ListErrors;
46
+
47
+ /** One user. */
48
+ @get
49
+ get(@path id: uuid): User | GetErrors;
50
+
51
+ /** Creates a user. */
52
+ @post
53
+ create(@body user: Create<User>): {
54
+ @statusCode _: 201;
55
+ @body user: User;
56
+ } | CreateErrors;
57
+
58
+ /** Updates a user with a merge patch. */
59
+ @patch
60
+ update(
61
+ @path id: uuid,
62
+ @body user: MergePatchUpdate<User>,
63
+ ): User | UpdateErrors;
64
+
65
+ /** Deletes a user. */
66
+ @delete
67
+ delete(@path id: uuid): NoContentResponse | DeleteErrors;
68
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from '@nxgt/openapi-codegen';
2
+
3
+ export default defineConfig({
4
+ input: 'openapi/openapi.yaml',
5
+ output: 'src/generated',
6
+ hono: true,
7
+ });
@@ -0,0 +1,37 @@
1
+ {
2
+ "nxgt": {
3
+ "title": "nxgt REST API",
4
+ "description": "A REST API with the @nxgt conventions: ids as written, pages, the error envelope and the linter, generated with @nxgt/openapi-codegen.",
5
+ "compilerVersion": "1.16.0",
6
+ "libraries": [
7
+ "@typespec/http",
8
+ "@typespec/openapi",
9
+ "@nxgt/typespec",
10
+ "@nxgt/openapi-codegen"
11
+ ],
12
+ "config": {
13
+ "linter": {
14
+ "extends": ["@nxgt/typespec/recommended"]
15
+ }
16
+ },
17
+ "emitters": {
18
+ "@typespec/openapi3": {
19
+ "selected": true,
20
+ "label": "OpenAPI 3.1 document",
21
+ "options": {
22
+ "emitter-output-dir": "{project-root}/openapi",
23
+ "openapi-versions": ["3.1.0"],
24
+ "output-file": "openapi.yaml"
25
+ }
26
+ }
27
+ },
28
+ "files": [
29
+ { "path": "nxgt/main.tsp", "destination": "main.tsp" },
30
+ {
31
+ "path": "nxgt/openapi-codegen.config.ts",
32
+ "destination": "openapi-codegen.config.ts"
33
+ },
34
+ { "path": "nxgt/README.md", "destination": "README.md" }
35
+ ]
36
+ }
37
+ }