From 68011665fdaa42c5bfbf80903cbff1bccf98569c Mon Sep 17 00:00:00 2001 From: Kengo Watanabe <121782456+nabeken5@users.noreply.github.com> Date: Sun, 21 Jan 2024 23:32:30 +0900 Subject: [PATCH] docs: Add JSDoc (#1916) * docs: Add JSDoc for App * docs: Add JSDoc for Routing * docs: Add JSDoc for Context * docs: Add JSDoc for HonoRequest * docs: Add JSDoc for Exception * add .header JSDoc & getPath JSDoc (`nabeken5:feat/add-jsdoc#1`) * add .header JSDoc & getPath JSDoc * fix: Apply the format to JSDoc --------- Thanks @EdamAme-x Co-authored-by: Kengo Watanabe <121782456+nabeken5@users.noreply.github.com> * refactor: format * feat: run denoify --------- Co-authored-by: Ame_x <121654029+EdamAme-x@users.noreply.github.com> --- deno_dist/context.ts | 177 ++++++++++++++++++++++++++++++++++++ deno_dist/hono-base.ts | 79 ++++++++++++++++ deno_dist/http-exception.ts | 18 ++++ deno_dist/request.ts | 161 ++++++++++++++++++++++++++++++++ src/context.ts | 177 ++++++++++++++++++++++++++++++++++++ src/hono-base.ts | 79 ++++++++++++++++ src/http-exception.ts | 18 ++++ src/request.ts | 161 ++++++++++++++++++++++++++++++++ 8 files changed, 870 insertions(+) diff --git a/deno_dist/context.ts b/deno_dist/context.ts index 728d15d9..44cbf375 100644 --- a/deno_dist/context.ts +++ b/deno_dist/context.ts @@ -95,10 +95,37 @@ export class Context< P extends string = any, I extends Input = {} > { + /** + * `.req` is the instance of {@link HonoRequest}. + */ req: HonoRequest + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + * @see https://hono.dev/api/context#env + */ env: E['Bindings'] = {} private _var: E['Variables'] = {} finalized: boolean = false + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + * @see https://hono.dev/api/context#error + */ error: Error | undefined = undefined #status: StatusCode = 200 @@ -122,6 +149,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#event + */ get event(): FetchEventLike { if (this.#executionCtx && 'respondWith' in this.#executionCtx) { return this.#executionCtx @@ -130,6 +160,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#executionctx + */ get executionCtx(): ExecutionContext { if (this.#executionCtx) { return this.#executionCtx as ExecutionContext @@ -138,6 +171,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#res + */ get res(): Response { this.#isFresh = false return (this.#res ||= new Response('404 Not Found', { status: 404 })) @@ -163,6 +199,40 @@ export class Context< this.finalized = true } + /** + * `.render()` can create a response within a layout. + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + * @see https://hono.dev/api/context#render-setrenderer + */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: Renderer = (...args: any[]) => this.renderer(...args) + + /** + * `.setRenderer()` can set the layout in the custom middleware. + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + * @see https://hono.dev/api/context#render-setrenderer + */ // @ts-expect-error It is unknown how many arguments the renderer will receive. // eslint-disable-next-line @typescript-eslint/no-explicit-any render: Renderer = (...args: any[]) => this.renderer(...args) @@ -174,6 +244,20 @@ export class Context< this.renderer = renderer } + /** + * `.header()` can set headers. + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * return c.body('Thank you for coming') + * }) + * ``` + * @see https://hono.dev/api/context#body + */ header = (name: string, value: string | undefined, options?: { append?: boolean }): void => { // Clear the header if (value === undefined) { @@ -218,15 +302,46 @@ export class Context< this.#status = status } + /** + * `.set()` can set the value specified by the key. + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is cool!!') + * await next() + * }) + * ``` + * @see https://hono.dev/api/context#set-get +``` + */ set: Set = (key: string, value: unknown) => { this._var ??= {} this._var[key as string] = value } + /** + * `.get()` can use the value specified by the key. + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + * @see https://hono.dev/api/context#set-get + */ get: Get = (key: string) => { return this._var ? this._var[key] : undefined } + /** + * `.var` can access the value of a variable. + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + * @see https://hono.dev/api/context#var + */ // c.var.propName is a read-only get var(): Readonly { return { ...this._var } as never @@ -283,6 +398,25 @@ export class Context< }) } + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + * @see https://hono.dev/api/context#body + */ body: BodyRespond = ( data: Data | null, arg?: StatusCode | ResponseInit, @@ -293,6 +427,16 @@ export class Context< : this.newResponse(data, arg) } + /** + * `.text()` can render text as `Content-Type:text/plain`. + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + * @see https://hono.dev/api/context#text + */ text: TextRespond = ( text: string, arg?: StatusCode | ResponseInit, @@ -312,6 +456,16 @@ export class Context< : this.newResponse(text, arg) } + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + * @see https://hono.dev/api/context#json + */ json: JSONRespond = ( object: InterfaceToType extends JSONValue ? T : JSONValue, arg?: StatusCode | ResponseInit, @@ -361,12 +515,35 @@ export class Context< : this.newResponse(html as string, arg) } + /** + * `.redirect()` can Redirect, default status code is 302. + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + * @see https://hono.dev/api/context#redirect + */ redirect = (location: string, status: StatusCode = 302): Response => { this.#headers ??= new Headers() this.#headers.set('Location', location) return this.newResponse(null, status) } + /** + * `.notFound()` can return the Not Found Response. + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + * @see https://hono.dev/api/context#notfound + */ notFound = (): Response | Promise => { return this.notFoundHandler(this) } diff --git a/deno_dist/hono-base.ts b/deno_dist/hono-base.ts index 0961204f..1475d46d 100644 --- a/deno_dist/hono-base.ts +++ b/deno_dist/hono-base.ts @@ -55,8 +55,38 @@ const errorHandler = (err: Error, c: Context) => { type GetPath = (request: Request, options?: { env?: E['Bindings'] }) => string export type HonoOptions = { + /** + * `strict` option specifies whether to distinguish whether the last path is a directory or not. + * @default true + * @see https://hono.dev/api/hono#strict-mode + */ strict?: boolean + /** + * `router` option specifices which router to use. + * ```ts + * const app = new Hono({ router: new RegExpRouter() }) + * ``` + * @see https://hono.dev/api/hono#router-option + */ router?: Router<[H, RouterRoute]> + /** + * `getPath` can handle the host header value. + * @example + * ```ts + * const app = new Hono({ + * getPath: (req) => + * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), + * }) + * + * app.get('/www1.example.com/hello', () => c.text('hello www1')) + * + * // A following request will match the route: + * // new Request('http://www1.example.com/hello', { + * // headers: { host: 'www1.example.com' }, + * // }) + * ``` + * @see https://hono.dev/api/routing#routing-with-host-header-value + */ getPath?: GetPath } @@ -179,17 +209,43 @@ class Hono< return this } + /** + * `.basePath()` allows base paths to be specified. + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + * @see https://hono.dev/api/routing#base-path + */ basePath(path: SubPath): Hono> { const subApp = this.clone() subApp._basePath = mergePath(this._basePath, path) return subApp } + /** + * `.onError()` handles an error and returns a customized Response. + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ onError = (handler: ErrorHandler) => { this.errorHandler = handler return this } + /** + * `.notFound()` allows you to customize a Not Found Response. + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + * @see https://hono.dev/api/hono#not-found + */ notFound = (handler: NotFoundHandler) => { this.notFoundHandler = handler return this @@ -314,10 +370,26 @@ class Hono< })() } + /** + * `.fetch()` will be entry point of your app. + * @see https://hono.dev/api/hono#fetch + */ fetch = (request: Request, Env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => { return this.dispatch(request, executionCtx, Env, request.method) } + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/api/hono#request + */ request = ( input: RequestInfo | URL, requestInit?: RequestInit, @@ -336,6 +408,13 @@ class Hono< return this.fetch(req, Env, executionCtx) } + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @see https://hono.dev/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ fire = () => { // @ts-expect-error `event` is not the type expected by addEventListener addEventListener('fetch', (event: FetchEventLike): void => { diff --git a/deno_dist/http-exception.ts b/deno_dist/http-exception.ts index 074bbeb2..3a6e3da9 100644 --- a/deno_dist/http-exception.ts +++ b/deno_dist/http-exception.ts @@ -5,6 +5,24 @@ type HTTPExceptionOptions = { message?: string } +/** + * `HTTPException` must be used when a fatal error such as authentication failure occurs. + * @example + * ```ts + * import { HTTPException } from 'hono/http-exception' + * + * // ... + * + * app.post('/auth', async (c, next) => { + * // authentication + * if (authorized === false) { + * throw new HTTPException(401, { message: 'Custom error message' }) + * } + * await next() + * }) + * ``` + * @see https://hono.dev/api/exception + */ export class HTTPException extends Error { readonly res?: Response readonly status: StatusCode diff --git a/deno_dist/request.ts b/deno_dist/request.ts index dbb20246..4cb54f4b 100644 --- a/deno_dist/request.ts +++ b/deno_dist/request.ts @@ -25,11 +25,33 @@ type Body = { type BodyCache = Partial export class HonoRequest

{ + /** + * `.raw` can get the raw Request object. + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + * @see https://hono.dev/api/request#raw + */ raw: Request #validatedData: { [K in keyof ValidationTargets]?: {} } // Short name of validatedData #matchResult: Result<[unknown, RouterRoute]> routeIndex: number = 0 + /** + * `.path` can get the pathname of the request. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + * @see https://hono.dev/api/request#path + */ path: string bodyCache: BodyCache = {} @@ -44,6 +66,16 @@ export class HonoRequest

{ this.#validatedData = {} } + /** + * `.req.param()` gets the path parameters. + * @example + * ```ts + * const name = c.req.param('name') + * // or all parameters at once + * const { id, comment_id } = c.req.param() + * ``` + * @see https://hono.dev/api/routing#path-parameter + */ param( key: RemoveQuestion> ): UndefinedIfHavingQuestion> @@ -76,18 +108,55 @@ export class HonoRequest

{ } } + /** + * `.query()` can get querystring parameters. + * @example + * ```ts + * // Query params + * app.get('/search', (c) => { + * const query = c.req.query('q') + * }) + * + * // Get all params at once + * app.get('/search', (c) => { + * const { q, limit, offset } = c.req.query() + * }) + * ``` + * @see https://hono.dev/api/request#query + */ query(key: string): string | undefined query(): Record query(key?: string) { return getQueryParam(this.url, key) } + /** + * `.queries()` can get multiple querystring parameter values, e.g. /search?tags=A&tags=B + * @example + * ```ts + * app.get('/search', (c) => { + * // tags will be string[] + * const tags = c.req.queries('tags') + * }) + * ``` + * @see https://hono.dev/api/request#queries + */ queries(key: string): string[] | undefined queries(): Record queries(key?: string) { return getQueryParams(this.url, key) } + /** + * `.header()` can get the request header value. + * @example + * ```ts + * app.get('/', (c) => { + * const userAgent = c.req.header('User-Agent') + * }) + * ``` + * @see https://hono.dev/api/request#header + */ header(name: string): string | undefined header(): Record header(name?: string) { @@ -100,6 +169,16 @@ export class HonoRequest

{ return headerData } + /** + * `.parseBody()` can parse Request body of type `multipart/form-data` or `application/x-www-form-urlencoded` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.parseBody() + * }) + * ``` + * @see https://hono.dev/api/request#parsebody + */ async parseBody(options?: ParseBodyOptions): Promise { if (this.bodyCache.parsedBody) return this.bodyCache.parsedBody as T const parsedBody = await parseBody(this, options) @@ -123,14 +202,44 @@ export class HonoRequest

{ return (bodyCache[key] = raw[key]()) } + /** + * `.json()` can parse Request body of type `application/json` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + * @see https://hono.dev/api/request#json + */ json(): Promise { return this.cachedBody('json') } + /** + * `.text()` can parse Request body of type `text/plain` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + * @see https://hono.dev/api/request#text + */ text(): Promise { return this.cachedBody('text') } + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + * @see https://hono.dev/api/request#arraybuffer + */ arrayBuffer(): Promise { return this.cachedBody('arrayBuffer') } @@ -152,18 +261,70 @@ export class HonoRequest

{ return this.#validatedData[target] as unknown } + /** + * `.url()` can get the request url strings. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + * @see https://hono.dev/api/request#url + */ get url() { return this.raw.url } + /** + * `.method()` can get the method name of the request. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + * @see https://hono.dev/api/request#method + */ get method() { return this.raw.method } + /** + * `.matchedRoutes()` can return a matched route in the handler + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + * @see https://hono.dev/api/request#matchedroutes + */ get matchedRoutes(): RouterRoute[] { return this.#matchResult[0].map(([[, route]]) => route) } + /** + * `routePath()` can retrieve the path registered within the handler + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + * @see https://hono.dev/api/request#routepath + */ get routePath(): string { return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path } diff --git a/src/context.ts b/src/context.ts index 4048cd92..f5dc2525 100644 --- a/src/context.ts +++ b/src/context.ts @@ -95,10 +95,37 @@ export class Context< P extends string = any, I extends Input = {} > { + /** + * `.req` is the instance of {@link HonoRequest}. + */ req: HonoRequest + /** + * `.env` can get bindings (environment variables, secrets, KV namespaces, D1 database, R2 bucket etc.) in Cloudflare Workers. + * @example + * ```ts + * // Environment object for Cloudflare Workers + * app.get('*', async c => { + * const counter = c.env.COUNTER + * }) + * ``` + * @see https://hono.dev/api/context#env + */ env: E['Bindings'] = {} private _var: E['Variables'] = {} finalized: boolean = false + /** + * `.error` can get the error object from the middleware if the Handler throws an error. + * @example + * ```ts + * app.use('*', async (c, next) => { + * await next() + * if (c.error) { + * // do something... + * } + * }) + * ``` + * @see https://hono.dev/api/context#error + */ error: Error | undefined = undefined #status: StatusCode = 200 @@ -122,6 +149,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#event + */ get event(): FetchEventLike { if (this.#executionCtx && 'respondWith' in this.#executionCtx) { return this.#executionCtx @@ -130,6 +160,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#executionctx + */ get executionCtx(): ExecutionContext { if (this.#executionCtx) { return this.#executionCtx as ExecutionContext @@ -138,6 +171,9 @@ export class Context< } } + /** + * @see https://hono.dev/api/context#res + */ get res(): Response { this.#isFresh = false return (this.#res ||= new Response('404 Not Found', { status: 404 })) @@ -163,6 +199,40 @@ export class Context< this.finalized = true } + /** + * `.render()` can create a response within a layout. + * @example + * ```ts + * app.get('/', (c) => { + * return c.render('Hello!') + * }) + * ``` + * @see https://hono.dev/api/context#render-setrenderer + */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: Renderer = (...args: any[]) => this.renderer(...args) + + /** + * `.setRenderer()` can set the layout in the custom middleware. + * @example + * ```tsx + * app.use('*', async (c, next) => { + * c.setRenderer((content) => { + * return c.html( + * + * + *

{content}

+ * + * + * ) + * }) + * await next() + * }) + * ``` + * @see https://hono.dev/api/context#render-setrenderer + */ // @ts-expect-error It is unknown how many arguments the renderer will receive. // eslint-disable-next-line @typescript-eslint/no-explicit-any render: Renderer = (...args: any[]) => this.renderer(...args) @@ -174,6 +244,20 @@ export class Context< this.renderer = renderer } + /** + * `.header()` can set headers. + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * + * return c.body('Thank you for coming') + * }) + * ``` + * @see https://hono.dev/api/context#body + */ header = (name: string, value: string | undefined, options?: { append?: boolean }): void => { // Clear the header if (value === undefined) { @@ -218,15 +302,46 @@ export class Context< this.#status = status } + /** + * `.set()` can set the value specified by the key. + * @example + * ```ts + * app.use('*', async (c, next) => { + * c.set('message', 'Hono is cool!!') + * await next() + * }) + * ``` + * @see https://hono.dev/api/context#set-get +``` + */ set: Set = (key: string, value: unknown) => { this._var ??= {} this._var[key as string] = value } + /** + * `.get()` can use the value specified by the key. + * @example + * ```ts + * app.get('/', (c) => { + * const message = c.get('message') + * return c.text(`The message is "${message}"`) + * }) + * ``` + * @see https://hono.dev/api/context#set-get + */ get: Get = (key: string) => { return this._var ? this._var[key] : undefined } + /** + * `.var` can access the value of a variable. + * @example + * ```ts + * const result = c.var.client.oneMethod() + * ``` + * @see https://hono.dev/api/context#var + */ // c.var.propName is a read-only get var(): Readonly { return { ...this._var } as never @@ -283,6 +398,25 @@ export class Context< }) } + /** + * `.body()` can return the HTTP response. + * You can set headers with `.header()` and set HTTP status code with `.status`. + * This can also be set in `.text()`, `.json()` and so on. + * @example + * ```ts + * app.get('/welcome', (c) => { + * // Set headers + * c.header('X-Message', 'Hello!') + * c.header('Content-Type', 'text/plain') + * // Set HTTP status code + * c.status(201) + * + * // Return the response body + * return c.body('Thank you for coming') + * }) + * ``` + * @see https://hono.dev/api/context#body + */ body: BodyRespond = ( data: Data | null, arg?: StatusCode | ResponseInit, @@ -293,6 +427,16 @@ export class Context< : this.newResponse(data, arg) } + /** + * `.text()` can render text as `Content-Type:text/plain`. + * @example + * ```ts + * app.get('/say', (c) => { + * return c.text('Hello!') + * }) + * ``` + * @see https://hono.dev/api/context#text + */ text: TextRespond = ( text: string, arg?: StatusCode | ResponseInit, @@ -312,6 +456,16 @@ export class Context< : this.newResponse(text, arg) } + /** + * `.json()` can render JSON as `Content-Type:application/json`. + * @example + * ```ts + * app.get('/api', (c) => { + * return c.json({ message: 'Hello!' }) + * }) + * ``` + * @see https://hono.dev/api/context#json + */ json: JSONRespond = ( object: InterfaceToType extends JSONValue ? T : JSONValue, arg?: StatusCode | ResponseInit, @@ -361,12 +515,35 @@ export class Context< : this.newResponse(html as string, arg) } + /** + * `.redirect()` can Redirect, default status code is 302. + * @example + * ```ts + * app.get('/redirect', (c) => { + * return c.redirect('/') + * }) + * app.get('/redirect-permanently', (c) => { + * return c.redirect('/', 301) + * }) + * ``` + * @see https://hono.dev/api/context#redirect + */ redirect = (location: string, status: StatusCode = 302): Response => { this.#headers ??= new Headers() this.#headers.set('Location', location) return this.newResponse(null, status) } + /** + * `.notFound()` can return the Not Found Response. + * @example + * ```ts + * app.get('/notfound', (c) => { + * return c.notFound() + * }) + * ``` + * @see https://hono.dev/api/context#notfound + */ notFound = (): Response | Promise => { return this.notFoundHandler(this) } diff --git a/src/hono-base.ts b/src/hono-base.ts index 1402bc61..d022ba74 100644 --- a/src/hono-base.ts +++ b/src/hono-base.ts @@ -55,8 +55,38 @@ const errorHandler = (err: Error, c: Context) => { type GetPath = (request: Request, options?: { env?: E['Bindings'] }) => string export type HonoOptions = { + /** + * `strict` option specifies whether to distinguish whether the last path is a directory or not. + * @default true + * @see https://hono.dev/api/hono#strict-mode + */ strict?: boolean + /** + * `router` option specifices which router to use. + * ```ts + * const app = new Hono({ router: new RegExpRouter() }) + * ``` + * @see https://hono.dev/api/hono#router-option + */ router?: Router<[H, RouterRoute]> + /** + * `getPath` can handle the host header value. + * @example + * ```ts + * const app = new Hono({ + * getPath: (req) => + * '/' + req.headers.get('host') + req.url.replace(/^https?:\/\/[^/]+(\/[^?]*)/, '$1'), + * }) + * + * app.get('/www1.example.com/hello', () => c.text('hello www1')) + * + * // A following request will match the route: + * // new Request('http://www1.example.com/hello', { + * // headers: { host: 'www1.example.com' }, + * // }) + * ``` + * @see https://hono.dev/api/routing#routing-with-host-header-value + */ getPath?: GetPath } @@ -179,17 +209,43 @@ class Hono< return this } + /** + * `.basePath()` allows base paths to be specified. + * @example + * ```ts + * const api = new Hono().basePath('/api') + * ``` + * @see https://hono.dev/api/routing#base-path + */ basePath(path: SubPath): Hono> { const subApp = this.clone() subApp._basePath = mergePath(this._basePath, path) return subApp } + /** + * `.onError()` handles an error and returns a customized Response. + * ```ts + * app.onError((err, c) => { + * console.error(`${err}`) + * return c.text('Custom Error Message', 500) + * }) + * ``` + */ onError = (handler: ErrorHandler) => { this.errorHandler = handler return this } + /** + * `.notFound()` allows you to customize a Not Found Response. + * ```ts + * app.notFound((c) => { + * return c.text('Custom 404 Message', 404) + * }) + * ``` + * @see https://hono.dev/api/hono#not-found + */ notFound = (handler: NotFoundHandler) => { this.notFoundHandler = handler return this @@ -314,10 +370,26 @@ class Hono< })() } + /** + * `.fetch()` will be entry point of your app. + * @see https://hono.dev/api/hono#fetch + */ fetch = (request: Request, Env?: E['Bindings'] | {}, executionCtx?: ExecutionContext) => { return this.dispatch(request, executionCtx, Env, request.method) } + /** + * `.request()` is a useful method for testing. + * You can pass a URL or pathname to send a GET request. + * app will return a Response object. + * ```ts + * test('GET /hello is ok', async () => { + * const res = await app.request('/hello') + * expect(res.status).toBe(200) + * }) + * ``` + * @see https://hono.dev/api/hono#request + */ request = ( input: RequestInfo | URL, requestInit?: RequestInit, @@ -336,6 +408,13 @@ class Hono< return this.fetch(req, Env, executionCtx) } + /** + * `.fire()` automatically adds a global fetch event listener. + * This can be useful for environments that adhere to the Service Worker API, such as non-ES module Cloudflare Workers. + * @see https://hono.dev/api/hono#fire + * @see https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API + * @see https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/ + */ fire = () => { // @ts-expect-error `event` is not the type expected by addEventListener addEventListener('fetch', (event: FetchEventLike): void => { diff --git a/src/http-exception.ts b/src/http-exception.ts index 88739918..ca40071f 100644 --- a/src/http-exception.ts +++ b/src/http-exception.ts @@ -5,6 +5,24 @@ type HTTPExceptionOptions = { message?: string } +/** + * `HTTPException` must be used when a fatal error such as authentication failure occurs. + * @example + * ```ts + * import { HTTPException } from 'hono/http-exception' + * + * // ... + * + * app.post('/auth', async (c, next) => { + * // authentication + * if (authorized === false) { + * throw new HTTPException(401, { message: 'Custom error message' }) + * } + * await next() + * }) + * ``` + * @see https://hono.dev/api/exception + */ export class HTTPException extends Error { readonly res?: Response readonly status: StatusCode diff --git a/src/request.ts b/src/request.ts index e4440116..c2a9d2ba 100644 --- a/src/request.ts +++ b/src/request.ts @@ -25,11 +25,33 @@ type Body = { type BodyCache = Partial export class HonoRequest

{ + /** + * `.raw` can get the raw Request object. + * @example + * ```ts + * // For Cloudflare Workers + * app.post('/', async (c) => { + * const metadata = c.req.raw.cf?.hostMetadata? + * ... + * }) + * ``` + * @see https://hono.dev/api/request#raw + */ raw: Request #validatedData: { [K in keyof ValidationTargets]?: {} } // Short name of validatedData #matchResult: Result<[unknown, RouterRoute]> routeIndex: number = 0 + /** + * `.path` can get the pathname of the request. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const pathname = c.req.path // `/about/me` + * }) + * ``` + * @see https://hono.dev/api/request#path + */ path: string bodyCache: BodyCache = {} @@ -44,6 +66,16 @@ export class HonoRequest

{ this.#validatedData = {} } + /** + * `.req.param()` gets the path parameters. + * @example + * ```ts + * const name = c.req.param('name') + * // or all parameters at once + * const { id, comment_id } = c.req.param() + * ``` + * @see https://hono.dev/api/routing#path-parameter + */ param( key: RemoveQuestion> ): UndefinedIfHavingQuestion> @@ -76,18 +108,55 @@ export class HonoRequest

{ } } + /** + * `.query()` can get querystring parameters. + * @example + * ```ts + * // Query params + * app.get('/search', (c) => { + * const query = c.req.query('q') + * }) + * + * // Get all params at once + * app.get('/search', (c) => { + * const { q, limit, offset } = c.req.query() + * }) + * ``` + * @see https://hono.dev/api/request#query + */ query(key: string): string | undefined query(): Record query(key?: string) { return getQueryParam(this.url, key) } + /** + * `.queries()` can get multiple querystring parameter values, e.g. /search?tags=A&tags=B + * @example + * ```ts + * app.get('/search', (c) => { + * // tags will be string[] + * const tags = c.req.queries('tags') + * }) + * ``` + * @see https://hono.dev/api/request#queries + */ queries(key: string): string[] | undefined queries(): Record queries(key?: string) { return getQueryParams(this.url, key) } + /** + * `.header()` can get the request header value. + * @example + * ```ts + * app.get('/', (c) => { + * const userAgent = c.req.header('User-Agent') + * }) + * ``` + * @see https://hono.dev/api/request#header + */ header(name: string): string | undefined header(): Record header(name?: string) { @@ -100,6 +169,16 @@ export class HonoRequest

{ return headerData } + /** + * `.parseBody()` can parse Request body of type `multipart/form-data` or `application/x-www-form-urlencoded` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.parseBody() + * }) + * ``` + * @see https://hono.dev/api/request#parsebody + */ async parseBody(options?: ParseBodyOptions): Promise { if (this.bodyCache.parsedBody) return this.bodyCache.parsedBody as T const parsedBody = await parseBody(this, options) @@ -123,14 +202,44 @@ export class HonoRequest

{ return (bodyCache[key] = raw[key]()) } + /** + * `.json()` can parse Request body of type `application/json` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.json() + * }) + * ``` + * @see https://hono.dev/api/request#json + */ json(): Promise { return this.cachedBody('json') } + /** + * `.text()` can parse Request body of type `text/plain` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.text() + * }) + * ``` + * @see https://hono.dev/api/request#text + */ text(): Promise { return this.cachedBody('text') } + /** + * `.arrayBuffer()` parse Request body as an `ArrayBuffer` + * @example + * ```ts + * app.post('/entry', async (c) => { + * const body = await c.req.arrayBuffer() + * }) + * ``` + * @see https://hono.dev/api/request#arraybuffer + */ arrayBuffer(): Promise { return this.cachedBody('arrayBuffer') } @@ -152,18 +261,70 @@ export class HonoRequest

{ return this.#validatedData[target] as unknown } + /** + * `.url()` can get the request url strings. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const url = c.req.url // `http://localhost:8787/about/me` + * ... + * }) + * ``` + * @see https://hono.dev/api/request#url + */ get url() { return this.raw.url } + /** + * `.method()` can get the method name of the request. + * @example + * ```ts + * app.get('/about/me', (c) => { + * const method = c.req.method // `GET` + * }) + * ``` + * @see https://hono.dev/api/request#method + */ get method() { return this.raw.method } + /** + * `.matchedRoutes()` can return a matched route in the handler + * @example + * ```ts + * app.use('*', async function logger(c, next) { + * await next() + * c.req.matchedRoutes.forEach(({ handler, method, path }, i) => { + * const name = handler.name || (handler.length < 2 ? '[handler]' : '[middleware]') + * console.log( + * method, + * ' ', + * path, + * ' '.repeat(Math.max(10 - path.length, 0)), + * name, + * i === c.req.routeIndex ? '<- respond from here' : '' + * ) + * }) + * }) + * ``` + * @see https://hono.dev/api/request#matchedroutes + */ get matchedRoutes(): RouterRoute[] { return this.#matchResult[0].map(([[, route]]) => route) } + /** + * `routePath()` can retrieve the path registered within the handler + * @example + * ```ts + * app.get('/posts/:id', (c) => { + * return c.json({ path: c.req.routePath }) + * }) + * ``` + * @see https://hono.dev/api/request#routepath + */ get routePath(): string { return this.#matchResult[0].map(([[, route]]) => route)[this.routeIndex].path }