Skip to content

Add initial server docs #6742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: internal
packages:
- "@typespec/http-server-csharp"
- "@typespec/http-server-js"
---

Update to generate emitter docs
2 changes: 1 addition & 1 deletion packages/http-server-csharp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"regen-docs": "echo No doc generation for alpha version",
"regen-docs": "tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/servers/http-server-csharp/reference --skip-js",
"emit:scenarios": "tsx eng/scripts/emit-scenarios.js --build true",
"test:scenarios": "vitest run --config ./vitest.config.scenarios.js"
},
Expand Down
127 changes: 127 additions & 0 deletions packages/http-server-js/.tspd/docs/footer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
## Functionality and generated code

The emitter generates a few major components:

### Router

The highest-level component that your code interacts with directly is the router implementation.
`@typespec/http-server-js` generates a static router that you can bind to an implementation of an HTTP server.

The router is generated in the `http/router.js` module within the output directory. Each service will have its own
router implementation named after the service. For example, given a service namespace named `Todo`, the router module
will export a function `createTodoRouter`. This function creates an instance of a router that dispatches methods within
the `Todo` service.

```ts
import { createTodoRouter } from "../tsp-output/@typespec/http-server-js/http/router.js";

const router = createTodoRouter(users, todoItems, attachments);
```

As arguments, the `createTodoRouter` function expects implementations of the underlying service interfaces. These
interfaces are explained further in the next section.

Once the router is created, it is bound to an instance of the HTTP server. The router's `dispatch` method implements the
Node.js event handler signature for the `request` event on a Node.js HTTP server.

```ts
const server = http.createServer();

server.on("request", router.dispatch);

server.listen(8080, () => {
console.log("Server listening on http://localhost:8080");
});
```

Alternatively, the router can be used with Express.js instead of the Node.js HTTP server directly. If the `express`
feature is enabled in the emitter options, the router will expose an `expressMiddleware` property that implements the
Express.js middleware interface.

```ts
import express from "express";

const app = express();

app.use(router.expressMiddleware);

app.listen(8080, () => {
console.log("Server listening on http://localhost:8080");
});
```

### Service interfaces

The emitter generates interfaces for each collection of service methods that exists in the service namespace.
Implementations of these interfaces are required to instantiate the router. When the router processes an HTTP request,
it will call the appropriate method on the service implementation after determining the route and method.

For example, given the following TypeSpec namespace `Users` within the `Todo` service:

```tsp
namespace Users {
@route("/users")
@post
op create(
user: User,
): WithStandardErrors<UserCreatedResponse | UserExistsResponse | InvalidUserResponse>;
}
```

The emitter will generate a corresponding interface `Users` within the module `models/all/todo/index.js` in the output
directory.

```ts
/** An interface representing the operations defined in the 'Todo.Users' namespace. */
export interface Users<Context = unknown> {
create(
ctx: Context,
user: User,
): Promise<
| UserCreatedResponse
| UserExistsResponse
| InvalidUserResponse
| Standard4XxResponse
| Standard5XxResponse
>;
}
```

An object implementing this `Users` interface must be passed to the router when it is created. The `Context` type
parameter represents the underlying protocol or framework-specific context that the service implementation may inspect.
If you need to access the HTTP request or response objects directly in the implementation of the service methods, you
must use the `HttpContext` type as the `Context` argument when implementing the service interface. Otherwise, it is safe
to use the default `unknown` argument.

```ts
import { HttpContext } from "../tsp-output/@typespec/http-server-js/helpers/router.js";
import { Users } from "../tsp-output/@typespec/http-server-js/models/all/todo/index.js";

export const users: Users<HttpContext> = {
async create(ctx, user) {
// Implementation
},
};
```

### Models

The emitter generates TypeScript interfaces that represent the model types used in the service operations. This allows
the service implementation to interact with the data structures carried over the HTTP protocol in a type-safe manner.

### Operation functions

While your code should never need to interact with these functions directly, the emitter generates a function per HTTP
operation that handles the parsing and validation of the request contents. This allows the service implementation to be
written in terms of ordinary TypeScript types and values rather than raw HTTP request and response objects. In general:

- The Node.js HTTP server or Express.js application (your code) calls the router (generated code), which determines
which service operation function (generated code) to call based on the route, method, and other HTTP metadata in the
case of shared routes.
- The operation function (generated code) deserializes the request body, query parameters, and headers into TypeScript
types, and may perform request validation.
- The operation function (generated code) calls the service implementation (your code) with the deserialized request
data.
- The service implementation (your code) returns a result or throws an error.
- The operation function (generated code) responds to the HTTP request on your behalf, converting the result or error
into HTTP response data.
1 change: 1 addition & 0 deletions packages/http-server-js/.tspd/docs/header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:warning: **This package is highly experimental and may be subject to breaking changes and bugs.** Please expect that your code may need to be updated as this package evolves, and please report any issues you encounter.
33 changes: 18 additions & 15 deletions packages/http-server-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,15 @@

:warning: **This package is highly experimental and may be subject to breaking changes and bugs.** Please expect that your code may need to be updated as this package evolves, and please report any issues you encounter.

TypeSpec HTTP server code generator for JavaScript and TypeScript.

This package generates an implementation of an HTTP server layer for a TypeSpec API. It supports binding directly to a
Node.js HTTP server or Express.js application.
TypeSpec HTTP server code generator for JavaScript

## Install

```bash
npm install @typespec/http-server-js
```

## Emitter

### Usage
## Usage

1. Via the command line

Expand All @@ -30,25 +25,33 @@ emit:
- "@typespec/http-server-js"
```

### Emitter options
The config can be extended with options as follows:

```yaml
emit:
- "@typespec/http-server-js"
options:
"@typespec/http-server-js":
option: value
```

## Emitter options

#### `express`
### `express`

**Type:** `boolean`

If set to `true`, the emitter will generate a router that exposes an Express.js middleware function in addition to the
ordinary Node.js HTTP server router.
If set to `true`, the emitter will generate a router that exposes an Express.js middleware function in addition to the ordinary Node.js HTTP server router.

If this option is not set to `true`, the `expressMiddleware` property will not be present on the generated router.

#### `omit-unreachable-types`
### `omit-unreachable-types`

**Type:** `boolean`

By default, the emitter will create interfaces that represent all models in the service namespace. If this option is set
to `true`, the emitter will only emit those types that are reachable from an HTTP operation.
By default, the emitter will create interfaces that represent all models in the service namespace. If this option is set to `true`, the emitter will only emit those types that are reachable from an HTTP operation.

#### `no-format`
### `no-format`

**Type:** `boolean`

Expand Down
7 changes: 5 additions & 2 deletions packages/http-server-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"type": "module",
"main": "dist/src/index.js",
"exports": {
".": "./dist/src/index.js",
".": {
"default": "./dist/src/index.js"
},
"./testing": "./dist/src/testing/index.js"
},
"bin": {
Expand All @@ -41,7 +43,7 @@
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"regen-docs": "echo Doc generation disabled for this package.",
"regen-docs": "tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/servers/http-server-js/reference --skip-js",
"test:e2e": "npm run emit:e2e && npm run run:e2e",
"emit:e2e": "node eng/scripts/emit-e2e.js",
"run:e2e": "vitest run --config ./vitest.config.e2e.js"
Expand All @@ -68,6 +70,7 @@
"@typespec/internal-build-utils": "workspace:^",
"@typespec/openapi3": "workspace:^",
"@typespec/spector": "workspace:^",
"@typespec/tspd": "workspace:^",
"@vitest/coverage-v8": "^3.0.9",
"@vitest/ui": "^3.0.9",
"fs-extra": "^11.2.0",
Expand Down
19 changes: 18 additions & 1 deletion packages/http-server-js/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,40 @@
import { JSONSchemaType, createTypeSpecLibrary, paramMessage } from "@typespec/compiler";

export interface JsEmitterOptions {
/** If set to `true`, the emitter will generate a router that exposes an Express.js middleware function in addition to the ordinary Node.js HTTP server router.

If this option is not set to `true`, the `expressMiddleware` property will not be present on the generated router. */
express?: boolean;

/** By default, the emitter will create interfaces that represent all models in the service namespace. If this option is set
to `true`, the emitter will only emit those types that are reachable from an HTTP operation. */
"omit-unreachable-types": boolean;
/** If set to `true`, the emitter will not format the generated code using Prettier. */
"no-format": boolean;
}

const EmitterOptionsSchema: JSONSchemaType<JsEmitterOptions> = {
type: "object",
additionalProperties: false,
properties: {
express: { type: "boolean", nullable: true, default: false },
express: {
type: "boolean",
nullable: true,
default: false,
description:
"If set to `true`, the emitter will generate a router that exposes an Express.js middleware function in addition to the ordinary Node.js HTTP server router.\n\nIf this option is not set to `true`, the `expressMiddleware` property will not be present on the generated router.",
},
"omit-unreachable-types": {
type: "boolean",
default: false,
description:
"By default, the emitter will create interfaces that represent all models in the service namespace. If this option is set to `true`, the emitter will only emit those types that are reachable from an HTTP operation.",
},
"no-format": {
type: "boolean",
default: false,
description:
"If set to `true`, the emitter will not format the generated code using Prettier.",
},
},
required: [],
Expand Down
16 changes: 2 additions & 14 deletions packages/http-specs/spec-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4610,13 +4610,7 @@ Expected input body:
- Endpoint: `get /type/model/visibility`

Generate and receive output model with readonly properties.
Expected input body:

```json
{
"queryProp": 123
}
```
Expected no body with `?queryProp=123`.

Expected response body:

Expand All @@ -4631,13 +4625,7 @@ Expected response body:
- Endpoint: `head /type/model/visibility`

Generate abd send put model with write/create properties.
Expected input body:

```json
{
"queryProp": 123
}
```
Expected no body with `?queryProp=123`.

### Type_Model_Visibility_patchModel

Expand Down
10 changes: 9 additions & 1 deletion packages/tspd/src/ref-doc/emitters/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ async function loadTemplate(projectRoot: string, name: string) {
export async function renderReadme(refDoc: TypeSpecRefDoc, projectRoot: string) {
const content: MarkdownDoc[] = [];
const renderer = new MarkdownRenderer(refDoc);

const headerTemplate = await loadTemplate(projectRoot, "header");
if (headerTemplate) {
content.push(headerTemplate);
}
if (refDoc.description) {
content.push(refDoc.description);
}
Expand All @@ -76,6 +79,11 @@ export async function renderReadme(refDoc: TypeSpecRefDoc, projectRoot: string)
content.push(section("Decorators", renderer.decoratorsSection(refDoc, { includeToc: true })));
}

const footerTemplate = await loadTemplate(projectRoot, "footer");
if (footerTemplate) {
content.push(footerTemplate);
}

return renderMarkdowDoc(section(refDoc.name, content));
}

Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@typespec/compiler": "workspace:^",
"@typespec/events": "workspace:^",
"@typespec/http": "workspace:^",
"@typespec/http-server-csharp": "workspace:^",
"@typespec/http-server-js": "workspace:^",
"@typespec/internal-build-utils": "workspace:^",
"@typespec/json-schema": "workspace:^",
"@typespec/openapi": "workspace:^",
Expand Down
Loading
Loading