diff --git a/.idea/typed-api-spec.iml b/.idea/typed-api-spec.iml index 69ef67a..d177006 100644 --- a/.idea/typed-api-spec.iml +++ b/.idea/typed-api-spec.iml @@ -6,6 +6,8 @@ + + diff --git a/docs/docs/01_overview.md b/docs/docs/01_overview.md new file mode 100644 index 0000000..12072b4 --- /dev/null +++ b/docs/docs/01_overview.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 1 +--- + +# Overview + +typed-api-spec is an TypeScript based declarative API specification. + +## Features + +### TypeScript based API Specification +You can define your API specification using TypeScript. +No need to use yaml, json, or any other format. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + responses: { 200: { body: { userNames: string[] }; }; }; + }; + }; +}>; +``` + +See [API Specification](./03_API%20Specification/specification) page for more details. + +### Type-safe, zero-runtime API client +typed-api-spec provides strict typed [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch). +For example, if you define API response schema by [API Specification](./03_API%20Specification/specification), you can get the typed response data. +It is just native [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), so: +- zero runtime: 0kb bundle size +- zero dependencies +- zero learning cost + +```typescript +const fetchT = fetch as FetchT<"", Spec>; +const res = await fetchT("/users"); +const data = await res.json(); // data is { userNames: string[] } +``` + +See [Client](./04_client/concept.md) page for more details. + +### Server Framework & Validation library integration +typed-api-spec can be integrated with various server frameworks(like [Express](https://expressjs.com/), [Fastify](https://fastify.dev/)) and validation libraries(like [zod](https://zod.dev/), [Valibot](https://valibot.dev/)). + +See [Server](/typed-api-spec/docs/category/server) and [Validation](/typed-api-spec/docs/category/validation) page for more details. \ No newline at end of file diff --git a/docs/docs/02_getting-started.md b/docs/docs/02_getting-started.md new file mode 100644 index 0000000..4bbdb64 --- /dev/null +++ b/docs/docs/02_getting-started.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 2 +--- + +# Getting Started + +Let's assume you have a npm project with code like this that __fetch__ from GitHub: + +```typescript +const GITHUB_API_ORIGIN = "https://api.github.com"; +const url = `/repos/mpppk/typed-api-spec/topics?page=1`; +const response = await fetch(`${GITHUB_API_ORIGIN}${url}`); +if (!response.ok) { + // response.json() returns any + const { message } = await response.json() + return console.error(message); +} +// response.json() returns any +const { names } = await response.json() +console.log(names); // => ["api-spec", "fetch", "typescript"] +``` + +## Installation + +```bash +npm install @mpppk/typed-api-spec +``` + +## Define API Spec + +```typescript +type Spec = DefineApiEndpoints<{ + "/repos/:owner/:repo/topics": { + get: { + query: { page?: string }; + responses: { + 200: { body: { names: string[] }; }; + 400: { body: { message: string; }; }; + }; + }; + }; +}>; +``` + +## Add types to fetch function + +```typescript +import { fetchT } from "@mpppk/typed-api-spec"; +const fetch = fetch as FetchT; +``` + +## Use fetch function + +```typescript {3} +const GITHUB_API_ORIGIN = "https://api.github.com"; +const url = `/repos/mpppk/typed-api-spec/topics?page=1`; +const fetchT = fetch as FetchT; +const response = await fetchT(`${GITHUB_API_ORIGIN}${url}`); +if (!response.ok) { + // reponse.json() is typed as { message: string } because resnose is not ok + const { message } = await response.json() + return console.error(message); +} +// reponse.json() is typed as { names: string[] } because resnose is ok +const { names } = await response.json() +console.log(names); // => ["api-spec", "fetch", "typescript"] +``` + +Notice that only few (highlighted) lines have been changed from original, but now the __fetch__ is type-safe. \ No newline at end of file diff --git a/docs/docs/03_api_specification.md b/docs/docs/03_api_specification.md new file mode 100644 index 0000000..e28bb03 --- /dev/null +++ b/docs/docs/03_api_specification.md @@ -0,0 +1,149 @@ +--- +sidebar_position: 3 +--- + +# API Specification + +API specification is a definition of the API endpoint. +In typed-api-spec, API specification is written in TypeScript type and has the following structure: + +``` +Path --> Method --> Spec +``` + +- Path: The path of the API endpoint. +- Method: The HTTP method of the API endpoint. +- Spec: The specification of the API endpoint, which includes request parameters, response schema, etc. + +For example, if you write the following code: + +```typescript +type Spec = DefineApiEndpoints<{ + // Path + "/users": { + // Method + get: { + // Spec + responses: { 200: { body: { userNames: string[] }; }; }; + }; + }; +}>; +``` + +the above code defines: +- Path: `/users` +- Method: `get` +- Spec: `{ responses: { 200: { body: { userNames: string[] }; }; }; }` + +## Structure + +### Path + +Path is the path of the API endpoint. +It can contain path parameters like `:id`. + +```typescript + +type Spec = DefineApiEndpoints<{ + // Path has path parameter `:id` + "/users/:id": { + get: { + params: { id: string }; + responses: { 200: { body: { user: { id: string; name: string }; }; }; }; + }; + }; +}>; +``` + +### Method + +Method is the HTTP method of the API endpoint. +It can be one of the following: +- `get` +- `post` +- `put` +- `delete` +- `patch` +- `head` +- `options` + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { responses: { 200: { body: { userNames: string[] }; }; }; }; + post: { responses: { 200: { body: { userName: string }; }; }; }; + }; +}>; +``` + +### Spec + +Spec has the following properties: +- `params`: The path parameters of the request. +- `query`: The query parameters of the request. +- `headers`: The headers of the request. +- `body`: The body of the request. +- `responses`: The response schema of the request. + - `body`: The body of the response. + - `headers`: The headers of the response. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users/:id": { + get: { + params: { id: string }; + query: { page?: string }; + headers: { "x-api-key": string }; + responses: { 200: { + headers: {"content-type": "application/json"}; + body: { userNames: string[] }; }; + }; + }; + }; +}>; +``` + +## Validation library integration + +typed-api-spec can be integrated with various validation libraries. +For example, you can use zod to define the schema of the request and response. + +```typescript +import { z } from "zod"; +import { ZodApiEndpoints } from "./index"; + +const Spec = { + "/users/:id": { + get: { + params: { id: z.string() }, + query: { page: z.string().optional() }, + headers: { "x-api-key": z.string() }, + responses: { + 200: { + headers: { "content-type": z.literal("application/json") }, + body: { userNames: z.array(z.string()) }, + } + }, + }, + }, +} satisfies ZodApiEndpoints +``` + +For more information, see the [Validation](/typed-api-spec/docs/category/validation) page. + +## API + +### DefineApiEndpoints + +DefineApiEndpoints is a utility type that defines the API specification. +If you write wrong API specification, DefineApiEndpoints will throw a type error. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { responses: { 200: { body: { userNames: string[] }; }; }; }; + }; +}>; +``` + +TODO: Add StackBlitz demo \ No newline at end of file diff --git a/docs/docs/04_client.md b/docs/docs/04_client.md new file mode 100644 index 0000000..ecb129b --- /dev/null +++ b/docs/docs/04_client.md @@ -0,0 +1,178 @@ +--- +sidebar_position: 4 +--- + +# Client(zero-fetch) + +typed-api-spec provides `zero-fetch`, a type-safe, zero-runtime API client. + +:::info[What does **zero-runtime** mean?] + +zero-fetch just add type information to native fetch, and does not add any runtime code. +Type information is erased during compilation, so it does not affect the runtime behavior. +As a result, it does not increase bundle size and does not have any runtime dependencies. + +````typescript +// fetchT is just native fetch, so it does not have any additional runtime dependencies +const fetchT = fetch as FetchT<"", Spec>; +```` + +::: + +## Type-safe features + +### Response + +zero-fetch provides type information for the response data based on the API specification. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + responses: { 200: { body: { names: string[] }; }; }; + }; + }; +}>; + +const fetchT = fetch as FetchT<"", Spec>; +const res = await fetchT("/users"); +const data = await res.json(); // data is { userNames: string[] } +``` + +If the response have multiple status codes, response type is union of each status code type. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + responses: { + 200: { body: { names: string[] }; }; + 201: { body: { ok: boolean }; }; + 400: { body: { message: string; }; }; + 500: { body: { internalError: string; }; }; + }; + }; + } +}>; + +const fetchT = fetch as FetchT<"", Spec>; +const res = await fetchT("/users"); +if (!res.ok) { + // If res.ok is false, status code is 400 or 500 + // So res.json() returns { message: string } | { internalError: string } + const data = await res.json(); + return console.error(data); +} +// If res.ok is true, status code is 200 or 201 +// So res.json() returns { names: string[] } | { ok: boolean } +const data = await res.json(); // names is string[] +console.log(data); +``` + +### Path & Path parameters + +zero-fetch accepts only the path that is defined in the API specification. +Path parameters are also supported as `:paramName` in the path. + +```typescript + +type Spec = DefineApiEndpoints<{ + "/users": { + get: { responses: { 200: { body: { names: string[] }; }; }; }; + }; + "/users/:id": { + get: { responses: { 200: { body: { name: string }; }; }; }; + }; +}>; +const fetchT = fetch as FetchT<"", Spec>; + +await fetchT("/users"); // OK +await fetchT("/users/1"); // OK +await fetchT("/posts"); // Error: Argument of type '"/posts"' is not assignable to parameter of type '"/users" | "/users/:id"'. +await fetchT("/users/1/2"); // Error: Argument of type '"/users/1/2"' is not assignable to parameter of type '"/users" | "/users/:id"'. +``` + +### Query + +zero-fetch accepts only the query parameters that are defined in the API specification. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + query: { page: string }; + responses: { 200: { body: { names: string[] }; }; }; + }; + }; +}>; + +const fetchT = fetch as FetchT<"", Spec>; +await fetchT("/users?page=1"); // OK +await fetchT("/users"); // Error: Argument of type '"/users"' is not assignable to parameter of type '"/users?page=${string}"'. +``` + +### headers + +zero-fetch accepts only the headers that are defined in the API specification. + +```typescript +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + headers: { "x-api-key": string }; + responses: { 200: { body: { names: string[] }; }; }; + }; + }; +}>; +const fetchT = fetch as FetchT<"", Spec>; + +await fetchT("/users", { headers: { "x-api-key": "key" } }); // OK +await fetchT("/users", { headers: {} }); // Error: Type {} is not assignable to type '{ "x-api-key": string; }'. +``` + +### body + +zero-fetch accepts only the body that is defined in the API specification. +Please note that when converting an object to a string, you must use the `JSONT` type provided by typed-api-spec. + +```typescript +import { JSONT } from "@mpppk/typed-api-spec/json"; +type Spec = DefineApiEndpoints<{ + "/users": { + post: { + body: { name: string }; + responses: { 200: { body: { id: string }; }; }; + }; + }; +}>; +const fetchT = fetch as FetchT<"", Spec>; +const JSONT = JSON as JSONT; + +await fetchT("/users", { method: "POST", body: JSONT.stringify({ name: "name" }) }); // OK +await fetchT("/users", { method: "POST", body: JSONT.stringify({ name: 1 }) }); // Error: Type TypedString<{ userName: number; }> is not assignable to type TypedString<{ userName: string; }> +``` + +## API + +### FetchT + +FetchT is a type that adds type information to native fetch. +First generic parameter is the origin of the API server, and the second generic parameter is the API specification. + +```typescript +const fetchT = fetch as FetchT<"https://api.example.com", Spec>; +``` + +### JSONT + +JSONT is a type that adds type information to native JSON. +If you want to check body parameter type, you need to use JSONT.stringify to convert object to string. + +```typescript +import { JSONT } from "@mpppk/typed-api-spec/json"; +const JSONT = JSON as JSONT; +// body parameter type will be checked by using JSONT.stringify +await fetchT("/users", { method: "POST", body: JSONT.stringify({ name: "name" }) }); +``` + + diff --git a/docs/docs/05_server/_category_.json b/docs/docs/05_server/_category_.json new file mode 100644 index 0000000..e3b9b0f --- /dev/null +++ b/docs/docs/05_server/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Server", + "position": 5, + "link": { + "type": "generated-index", + "description": "typed-api-spec provides server integrations that support implementing handler based on API Specification." + } +} diff --git a/docs/docs/05_server/express.md b/docs/docs/05_server/express.md new file mode 100644 index 0000000..5354e00 --- /dev/null +++ b/docs/docs/05_server/express.md @@ -0,0 +1,83 @@ +# Express + +Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. + + +typed-api-spec provides first-class support for Express. +You can apply types to your Express app using the `typed()`. + +Here is an example of how to use `express` with `typed-api-spec` and `zod`. + +```typescript +const app = express(); +app.use(express.json()); +const wApp = typed(pathMap, app); +wApp.get("/users", (req, res) => { +// validate method is available in res.locals +// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query) +const { data, error } = res.locals.validate(req).query(); +if (data !== undefined) { + // res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"] + res.status(200).json({ userNames: [`page${data.page}#user1`] }); +} else { + // res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"] + res.status(400).json({ errorMessage: error.toString() }); +} +}); + +newApp().listen(3000, () => { console.log(`Example app listening on port ${port}`); }); +``` + +## API + +### typed() + +typed() is a function that applies more strict types to the Express app. +It returns an Express app same as the input app, but validate method has been added to the request locals. + +Note that the validate method is depended on validation library you use. +Following example uses zod. + +```typescript +import { ZodApiEndpoints } from "@mpppk/typed-api-spec/zod"; +import { typed } from "@mpppk/typed-api-spec/express/zod"; +import { z } from "zod"; + +const Spec = { + "/users": { + get: { + query: z.object({ page: z.string() }), + responses: { 200: { body: z.object({ userNames: z.string().array() }) } } + } + } +} satisfies ZodApiEndpoints + +const wApp = typed(pathMap, app); +wApp.get("users", (req, res) => { + // validate method is available in res.locals because of typed() + const { data, error } = res.locals.validate(req).query(); +}) +``` + +### validate() + +validate() is a method that is added to the request locals by typed(). +It returns a function that validates the request parameters and returns the result. +Available methods are `query()`, `params()`, `headers()`, and `body()`. + +### asAsync() + +asAsync() is a function that wraps the express app to handle errors of async handlers. +If error is thrown in async handler of wrapped app, it will be caught and passed to the error handler. + +:::note + +The reason why we provide this function is that Express4 or lower does not properly handle async errors. +The upcoming Express5 release will support async error handling, making this method unnecessary. + +::: + +## Supported validation libraries + +* [zod](/typed-api-spec/docs/validation/zod) +* [valibot](/typed-api-spec/docs/validation/valibot) \ No newline at end of file diff --git a/docs/docs/05_server/fastify.md b/docs/docs/05_server/fastify.md new file mode 100644 index 0000000..5d3039b --- /dev/null +++ b/docs/docs/05_server/fastify.md @@ -0,0 +1,62 @@ +# Fastify + +Fastify is a web framework highly focused on providing the best developer experience with the least overhead and a powerful plugin architecture. + +Here is an example of how to use `fastify` with `typed-api-spec` and `zod`. + +```typescript +const fastify = Fastify({ logger: true }); + +fastify.setValidatorCompiler(validatorCompiler); +fastify.setSerializerCompiler(serializerCompiler); +const server = fastify.withTypeProvider(); + +const routes = toRoutes(pathMap); + +server.route({ + ...routes["/users"]["get"], + handler: async (request, reply) => { + const page = request.query.page; + return { userNames: [`page${page}#user1`] }; + }, +}); + +await fastify.listen({ port: 3000 }); +``` + +## API + +### toRoutes() + +toRoutes() is a function that converts the API specification schema to Fastify route object. + +```typescript + +const Spec = { + "/users": { + get: { + query: z.object({ page: z.string() }), + responses: { 200: { body: z.object({ userNames: z.string().array() }) }}, + }, + }, +} satisfies ZodApiEndpoints + +const routes = toRoutes(Spec); +console.log(routes["/users"]["get"]) +/* => +{ + method: 'GET', + url: '/users', + schema: { + query: z.object({ page: { type: 'string' } }), + response: { + 200: { z.object({ userNames: z.string().array() } } + } + } +} +*/ +``` + +## Supported validation libraries + +* [zod](/typed-api-spec/docs/validation/zod) diff --git a/docs/docs/06_validation/_category_.json b/docs/docs/06_validation/_category_.json new file mode 100644 index 0000000..e58be27 --- /dev/null +++ b/docs/docs/06_validation/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Validation", + "position": 6, + "link": { + "type": "generated-index", + "description": "You can use your favorite validation library with typed-api-spec." + } +} diff --git a/docs/docs/06_validation/concept.md b/docs/docs/06_validation/concept.md new file mode 100644 index 0000000..184dd7c --- /dev/null +++ b/docs/docs/06_validation/concept.md @@ -0,0 +1,93 @@ +--- +sidebar_position: 0 +--- + +# Overview + +The most basic way to define an API specification in typed-api-spec is using TypeScript types. +However, you may want to perform runtime validation to check that request parameters are provided in the expected type. + +The simplest way is to use only the type information provided by a validation library. For exmple, you can use [zod](https://zod.dev) to define the API specification of typed-api-spec. + +```typescript + +import { z } from "zod"; + +const User = z.object({ + id: z.string(), + name: z.string(), +}); +type User = z.infer; + +const UsersQuery = z.object({ + page: z.string(), +}); + +type Spec = DefineApiEndpoints<{ + "/users": { + get: { + query: UsersQuery; + responses: { 200: { body: User } }; + }; + }; +}>; +``` + +In this way, you can completely separate runtime validation and API definition, allowing you to use any validation library. +To validate, just use the validation library. Following code is an example of how to validate request parameters using zod and express. + +```typescript +const app = Express(); +app.get("users", (req, res) => { + const usersQuery = UsersQuery.parse(req.query) +}) +``` + +This is not bad, but the problem is that there is no guarantee that the UsersQuery is actually the expected schema. For example, you may accidentally write something like `User.parse(req.query)`. +(`User` is expected as a response schema, not a query.) + +typed-api-spec provides a way to define the API specification using the validation library directly. + +```typescript +import { z } from "zod"; + +const Spec = { + "/users": { + get: { + query: z.object({ page: z.string() }), + responses: { 200: { body: z.object({ id: z.string(), name: z.string() }) } } + } + } +} satisfies ZodApiEndpoints +``` + +In this way, you can use validate function directly. + +```typescript +const app = Express(); +app.get("users", (req, res) => { + const usersQuery = Spec["/users"]["get"].query.parse(req.query) +}) +``` + +:::note[Server integration] + +You can make it even easier by using some server-oriented integrations such as [Express](/typed-api-spec/docs/server/express) and [Fastify]((/typed-api-spec/docs/server/fastify)). + +```typescript +const app = express(); +const wApp = typed(Spec, app); +wApp.get("users", (req, res) => { + const { data, error } = res.locals.validate(req).query(); +}) +``` + +For more information, see the [Server](/typed-api-spec/docs/category/server) page. + +::: + +:::note[You need validate explicitly if you want] + +Even if you define API Specification with a validation library, typed-api-spec does not validate the request parameters automatically. +You need to explicitly perform validation, whether on the server side or on the client side. +::: diff --git a/docs/docs/06_validation/valibot.md b/docs/docs/06_validation/valibot.md new file mode 100644 index 0000000..19dd3d2 --- /dev/null +++ b/docs/docs/06_validation/valibot.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 2 +--- + +# Valibot + +[valibot](https://valibot.dev ) is the open source schema library for TypeScript with bundle size, type safety and developer experience in mind. +You can use valibot to define the API specification of typed-api-spec. + +```typescript +import { v } from "valibot"; + +const Spec = { + "/users/:id": { + get: { + params: v.object({ id: v.string() }), + query: v.object({ page: v.string().optional() }), + headers: v.object({ "x-api-key": v.string() }), + responses: { + 200: { + headers: v.object({ "content-type": v.literal("application/json") }), + body: v.object({ userNames: v.array(v.string()) }), + } + }, + }, + }, +} satisfies ZodApiEndpoints +``` + +### Using server integration + +If you use `express`, you can use official integration to validate request parameters based on the API specification. +See the [express](/typed-api-spec/docs/server/express) page for more information. \ No newline at end of file diff --git a/docs/docs/06_validation/write.md b/docs/docs/06_validation/write.md new file mode 100644 index 0000000..e15af4e --- /dev/null +++ b/docs/docs/06_validation/write.md @@ -0,0 +1,6 @@ +# Write your own integration + +If you want to use a validation library which is not supported by `typed-api-spec`, you can write your own integration. +typed-api-spec provides a utility type `DefineApiSpec`, `DefineApiResponses`, and others for defining API specification schema with validation library. + +TBW \ No newline at end of file diff --git a/docs/docs/06_validation/zod.md b/docs/docs/06_validation/zod.md new file mode 100644 index 0000000..ceb5305 --- /dev/null +++ b/docs/docs/06_validation/zod.md @@ -0,0 +1,33 @@ +--- +sidebar_position: 1 +--- + +# zod + +[zod](https://zod.dev) is a TypeScript-first schema declaration and validation library. +You can use zod to define the API specification of typed-api-spec. + +```typescript +import { z } from "zod"; + +const Spec = { + "/users/:id": { + get: { + params: z.object({ id: z.string() }), + query: z.object({ page: z.string().optional() }), + headers: z.object({ "x-api-key": z.string() }), + responses: { + 200: { + headers: z.object({ "content-type": z.literal("application/json") }), + body: z.object({ userNames: z.array(z.string()) }), + } + }, + }, + }, +} satisfies ZodApiEndpoints +``` + +## Using server integration + +If you use `express` or `fastify`, you can use official integration to validate request parameters based on the API specification. +See the [express](/typed-api-spec/docs/server/express) or [fastify](/typed-api-spec/docs/server/fastify) page for more information. diff --git a/docs/docs/intro.md b/docs/docs/intro.md deleted file mode 100644 index 45e8604..0000000 --- a/docs/docs/intro.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Tutorial Intro - -Let's discover **Docusaurus in less than 5 minutes**. - -## Getting Started - -Get started by **creating a new site**. - -Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. - -### What you'll need - -- [Node.js](https://nodejs.org/en/download/) version 18.0 or above: - - When installing Node.js, you are recommended to check all checkboxes related to dependencies. - -## Generate a new site - -Generate a new Docusaurus site using the **classic template**. - -The classic template will automatically be added to your project after you run the command: - -```bash -npm init docusaurus@latest my-website classic -``` - -You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. - -The command also installs all necessary dependencies you need to run Docusaurus. - -## Start your site - -Run the development server: - -```bash -cd my-website -npm run start -``` - -The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. - -The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. - -Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes. diff --git a/docs/docs/tutorial-basics/_category_.json b/docs/docs/tutorial-basics/_category_.json deleted file mode 100644 index 2e6db55..0000000 --- a/docs/docs/tutorial-basics/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Tutorial - Basics", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Docusaurus concepts." - } -} diff --git a/docs/docs/tutorial-basics/congratulations.md b/docs/docs/tutorial-basics/congratulations.md deleted file mode 100644 index 04771a0..0000000 --- a/docs/docs/tutorial-basics/congratulations.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Congratulations! - -You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. - -Docusaurus has **much more to offer**! - -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. - -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) - -## What's next? - -- Read the [official documentation](https://docusaurus.io/) -- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) -- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) -- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) -- Add a [search bar](https://docusaurus.io/docs/search) -- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) -- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/docs/docs/tutorial-basics/create-a-blog-post.md b/docs/docs/tutorial-basics/create-a-blog-post.md deleted file mode 100644 index 550ae17..0000000 --- a/docs/docs/tutorial-basics/create-a-blog-post.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Create a Blog Post - -Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... - -## Create your first Post - -Create a file at `blog/2021-02-28-greetings.md`: - -```md title="blog/2021-02-28-greetings.md" ---- -slug: greetings -title: Greetings! -authors: - - name: Joel Marcey - title: Co-creator of Docusaurus 1 - url: https://github.com/JoelMarcey - image_url: https://github.com/JoelMarcey.png - - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png -tags: [greetings] ---- - -Congratulations, you have made your first post! - -Feel free to play around and edit this post as much as you like. -``` - -A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/docs/docs/tutorial-basics/create-a-document.md b/docs/docs/tutorial-basics/create-a-document.md deleted file mode 100644 index 5638bb6..0000000 --- a/docs/docs/tutorial-basics/create-a-document.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create a Document - -Documents are **groups of pages** connected through: - -- a **sidebar** -- **previous/next navigation** -- **versioning** - -## Create your first Doc - -Create a Markdown file at `docs/hello.md`: - -```md title="docs/hello.md" -# Hello - -This is my **first Docusaurus document**! -``` - -A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). - -## Configure the Sidebar - -Docusaurus automatically **creates a sidebar** from the `docs` folder. - -Add metadata to customize the sidebar label and position: - -```md title="docs/hello.md" {1-4} ---- -sidebar_label: "Hi!" -sidebar_position: 3 ---- - -# Hello - -This is my **first Docusaurus document**! -``` - -It is also possible to create your sidebar explicitly in `sidebars.js`: - -```js title="sidebars.js" -export default { - tutorialSidebar: [ - "intro", - // highlight-next-line - "hello", - { - type: "category", - label: "Tutorial", - items: ["tutorial-basics/create-a-document"], - }, - ], -}; -``` diff --git a/docs/docs/tutorial-basics/create-a-page.md b/docs/docs/tutorial-basics/create-a-page.md deleted file mode 100644 index a2cb8ed..0000000 --- a/docs/docs/tutorial-basics/create-a-page.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a Page - -Add **Markdown or React** files to `src/pages` to create a **standalone page**: - -- `src/pages/index.js` → `localhost:3000/` -- `src/pages/foo.md` → `localhost:3000/foo` -- `src/pages/foo/bar.js` → `localhost:3000/foo/bar` - -## Create your first React Page - -Create a file at `src/pages/my-react-page.js`: - -```jsx title="src/pages/my-react-page.js" -import React from "react"; -import Layout from "@theme/Layout"; - -export default function MyReactPage() { - return ( - -

My React page

-

This is a React page

-
- ); -} -``` - -A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). - -## Create your first Markdown Page - -Create a file at `src/pages/my-markdown-page.md`: - -```mdx title="src/pages/my-markdown-page.md" -# My Markdown page - -This is a Markdown page -``` - -A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/docs/docs/tutorial-basics/deploy-your-site.md b/docs/docs/tutorial-basics/deploy-your-site.md deleted file mode 100644 index 1c50ee0..0000000 --- a/docs/docs/tutorial-basics/deploy-your-site.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Deploy your site - -Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). - -It builds your site as simple **static HTML, JavaScript and CSS files**. - -## Build your site - -Build your site **for production**: - -```bash -npm run build -``` - -The static files are generated in the `build` folder. - -## Deploy your site - -Test your production build locally: - -```bash -npm run serve -``` - -The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). - -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/docs/docs/tutorial-basics/markdown-features.mdx b/docs/docs/tutorial-basics/markdown-features.mdx deleted file mode 100644 index 0c59028..0000000 --- a/docs/docs/tutorial-basics/markdown-features.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Markdown Features - -Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. - -## Front Matter - -Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): - -```text title="my-doc.md" -// highlight-start ---- -id: my-doc-id -title: My document title -description: My document description -slug: /my-custom-url ---- -// highlight-end - -## Markdown heading - -Markdown text with [links](./hello.md) -``` - -## Links - -Regular Markdown links are supported, using url paths or relative file paths. - -```md -Let's see how to [Create a page](/create-a-page). -``` - -```md -Let's see how to [Create a page](./create-a-page.md). -``` - -**Result:** Let's see how to [Create a page](./create-a-page.md). - -## Images - -Regular Markdown images are supported. - -You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): - -```md -![Docusaurus logo](/img/docusaurus.png) -``` - -![Docusaurus logo](/img/docusaurus.png) - -You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: - -```md -![Docusaurus logo](./img/docusaurus.png) -``` - -## Code Blocks - -Markdown code blocks are supported with Syntax highlighting. - -````md -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` -```` - -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` - -## Admonitions - -Docusaurus has a special syntax to create admonitions and callouts: - -```md -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: -``` - -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: - -## MDX and React Components - -[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: - -```jsx -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`) - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! -``` - -export const Highlight = ({ children, color }) => ( - { - alert(`You clicked the color ${color} with label ${children}`); - }} - > - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! diff --git a/docs/docs/tutorial-extras/_category_.json b/docs/docs/tutorial-extras/_category_.json deleted file mode 100644 index a8ffcc1..0000000 --- a/docs/docs/tutorial-extras/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Tutorial - Extras", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/tutorial-extras/img/docsVersionDropdown.png b/docs/docs/tutorial-extras/img/docsVersionDropdown.png deleted file mode 100644 index 97e4164..0000000 Binary files a/docs/docs/tutorial-extras/img/docsVersionDropdown.png and /dev/null differ diff --git a/docs/docs/tutorial-extras/img/localeDropdown.png b/docs/docs/tutorial-extras/img/localeDropdown.png deleted file mode 100644 index e257edc..0000000 Binary files a/docs/docs/tutorial-extras/img/localeDropdown.png and /dev/null differ diff --git a/docs/docs/tutorial-extras/manage-docs-versions.md b/docs/docs/tutorial-extras/manage-docs-versions.md deleted file mode 100644 index 9ea672c..0000000 --- a/docs/docs/tutorial-extras/manage-docs-versions.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Manage Docs Versions - -Docusaurus can manage multiple versions of your docs. - -## Create a docs version - -Release a version 1.0 of your project: - -```bash -npm run docusaurus docs:version 1.0 -``` - -The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created. - -Your docs now have 2 versions: - -- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs -- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs** - -## Add a Version Dropdown - -To navigate seamlessly across versions, add a version dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -export default { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: "docsVersionDropdown", - }, - // highlight-end - ], - }, - }, -}; -``` - -The docs version dropdown appears in your navbar: - -![Docs Version Dropdown](./img/docsVersionDropdown.png) - -## Update an existing version - -It is possible to edit versioned docs in their respective folder: - -- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello` -- `docs/hello.md` updates `http://localhost:3000/docs/next/hello` diff --git a/docs/docs/tutorial-extras/translate-your-site.md b/docs/docs/tutorial-extras/translate-your-site.md deleted file mode 100644 index cf3a0c4..0000000 --- a/docs/docs/tutorial-extras/translate-your-site.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Translate your site - -Let's translate `docs/intro.md` to French. - -## Configure i18n - -Modify `docusaurus.config.js` to add support for the `fr` locale: - -```js title="docusaurus.config.js" -export default { - i18n: { - defaultLocale: "en", - locales: ["en", "fr"], - }, -}; -``` - -## Translate a doc - -Copy the `docs/intro.md` file to the `i18n/fr` folder: - -```bash -mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ - -cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md -``` - -Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. - -## Start your localized site - -Start your site on the French locale: - -```bash -npm run start -- --locale fr -``` - -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. - -:::caution - -In development, you can only use one locale at a time. - -::: - -## Add a Locale Dropdown - -To navigate seamlessly across languages, add a locale dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -export default { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: "localeDropdown", - }, - // highlight-end - ], - }, - }, -}; -``` - -The locale dropdown now appears in your navbar: - -![Locale Dropdown](./img/localeDropdown.png) - -## Build your localized site - -Build your site for a specific locale: - -```bash -npm run build -- --locale fr -``` - -Or build your site to include all the locales at once: - -```bash -npm run build -``` diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index a8e0d90..3daa680 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -8,7 +8,7 @@ const config: Config = { "TypeScript based declarative API specification and zero-runtime client", favicon: "img/favicon.ico", - // Set the production url of your site here + // Set the poduction url of your site here url: "https://your-docusaurus-site.example.com", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' diff --git a/docs/static/img/typed-api-spec.png b/docs/static/img/typed-api-spec.png new file mode 100644 index 0000000..bb085f1 Binary files /dev/null and b/docs/static/img/typed-api-spec.png differ diff --git a/examples/github/github.ts b/examples/github/github.ts new file mode 100644 index 0000000..fdb648a --- /dev/null +++ b/examples/github/github.ts @@ -0,0 +1,54 @@ +import { DefineApiEndpoints, FetchT } from "../../src"; + +const GITHUB_API_ORIGIN = "https://api.github.com"; + +// There are more headers, but I omit them for simplicity +type ResponseHeaders = Record< + "x-ratelimit-remaining" | "x-ratelimit-reset" | "x-github-request-id", + string +>; + +// See https://docs.github.com/ja/rest/repos/repos?apiVersion=2022-11-28#get-all-repository-topics +type Spec = DefineApiEndpoints<{ + "/repos/:owner/:repo/topics": { + get: { + query: { page?: string }; + headers: { + Accept?: "application/vnd.github+json"; + Authorization?: `Bearer ${string}`; + "X-GitHub-Api-Version"?: "2022-11-28"; + }; + responses: { + 200: { + body: { names: string[] }; + headers: ResponseHeaders; + }; + 400: { + body: { + message: string; + errors: string; + documentation_url: string; + status: number; + }; + headers: ResponseHeaders; + }; + }; + }; + }; +}>; +const fetchT = fetch as FetchT; + +const main = async () => { + const response = await fetchT( + `${GITHUB_API_ORIGIN}/repos/mpppk/typed-api-spec/topics?page=1`, + { headers: { Accept: "application/vnd.github+json" } }, + ); + if (!response.ok) { + const { message } = await response.json(); + return console.error(message); + } + const { names } = await response.json(); + console.log(names); +}; + +main(); diff --git a/src/express/zod.ts b/src/express/zod.ts index 393c318..988146b 100644 --- a/src/express/zod.ts +++ b/src/express/zod.ts @@ -51,8 +51,8 @@ export type ToValidatorsMap = { * ``` * const router = typed(pathMap, express.Router()) * router.get('/path', (req, res) => { - * const r = res.locals.validate(req).query() - * if (!r.success) { + * const {data, error} = res.locals.validate(req).query() + * if (error) { * return res.status(400).json({ message: 'Invalid query' }) * } * return res.status(200).json({ message: 'success', value: r.data.value })