Comparing version 1.6.4 to 2.0.0
@@ -19,3 +19,3 @@ "use strict"; | ||
if (!handler) { | ||
if (context instanceof context_1.Context && context.finalized === false && onNotFound) { | ||
if (context instanceof context_1.HonoContext && context.finalized === false && onNotFound) { | ||
context.res = await onNotFound(context); | ||
@@ -28,3 +28,3 @@ } | ||
// If handler return Response like `return c.text('foo')` | ||
if (res && context instanceof context_1.Context) { | ||
if (res && context instanceof context_1.HonoContext) { | ||
context.res = res; | ||
@@ -35,3 +35,3 @@ } | ||
.catch((err) => { | ||
if (context instanceof context_1.Context && onError) { | ||
if (context instanceof context_1.HonoContext && onError) { | ||
if (err instanceof Error) { | ||
@@ -38,0 +38,0 @@ context.res = onError(err, context); |
/// <reference types="@cloudflare/workers-types" /> | ||
import type { NotFoundHandler } from './hono'; | ||
import type { CookieOptions } from './utils/cookie'; | ||
import type { StatusCode } from './utils/http-status'; | ||
@@ -7,3 +8,3 @@ declare type Headers = Record<string, string>; | ||
declare type Env = Record<string, any>; | ||
export declare class Context<RequestParamKeyType extends string = string, E = Env> { | ||
export interface Context<RequestParamKeyType extends string = string, E = Env> { | ||
req: Request<RequestParamKeyType>; | ||
@@ -14,3 +15,25 @@ env: E; | ||
finalized: boolean; | ||
private _status; | ||
get res(): Response; | ||
set res(_res: Response); | ||
header: (name: string, value: string) => void; | ||
status: (status: StatusCode) => void; | ||
set: (key: string, value: any) => void; | ||
get: (key: string) => any; | ||
pretty: (prettyJSON: boolean, space?: number) => void; | ||
newResponse: (data: Data | null, status: StatusCode, headers: Headers) => Response; | ||
body: (data: Data | null, status?: StatusCode, headers?: Headers) => Response; | ||
text: (text: string, status?: StatusCode, headers?: Headers) => Response; | ||
json: <T>(object: T, status?: StatusCode, headers?: Headers) => Response; | ||
html: (html: string, status?: StatusCode, headers?: Headers) => Response; | ||
redirect: (location: string, status?: StatusCode) => Response; | ||
cookie: (name: string, value: string, options?: CookieOptions) => void; | ||
notFound: () => Response | Promise<Response>; | ||
} | ||
export declare class HonoContext<RequestParamKeyType extends string = string, E = Env> implements Context<RequestParamKeyType, E> { | ||
req: Request<RequestParamKeyType>; | ||
env: E; | ||
event: FetchEvent | undefined; | ||
executionCtx: ExecutionContext | undefined; | ||
finalized: boolean; | ||
_status: StatusCode; | ||
private _pretty; | ||
@@ -36,4 +59,5 @@ private _prettySpace; | ||
redirect(location: string, status?: StatusCode): Response; | ||
cookie(name: string, value: string, opt?: CookieOptions): void; | ||
notFound(): Response | Promise<Response>; | ||
} | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Context = void 0; | ||
exports.HonoContext = void 0; | ||
const cookie_1 = require("./utils/cookie"); | ||
const url_1 = require("./utils/url"); | ||
class Context { | ||
class HonoContext { | ||
constructor(req, env = undefined, eventOrExecutionCtx = undefined, notFoundHandler = () => new Response()) { | ||
@@ -92,2 +93,6 @@ this._status = 200; | ||
} | ||
cookie(name, value, opt) { | ||
const cookie = (0, cookie_1.serialize)(name, value, opt); | ||
this.header('Set-Cookie', cookie); | ||
} | ||
notFound() { | ||
@@ -97,2 +102,2 @@ return this.notFoundHandler(this); | ||
} | ||
exports.Context = Context; | ||
exports.HonoContext = HonoContext; |
/// <reference types="@cloudflare/workers-types" /> | ||
import { Context } from './context'; | ||
import type { Context } from './context'; | ||
import type { Router } from './router'; | ||
@@ -4,0 +4,0 @@ declare type Env = Record<string, any>; |
@@ -103,3 +103,3 @@ "use strict"; | ||
const handlers = result ? result.handlers : [this.notFoundHandler]; | ||
const c = new context_1.Context(request, env, eventOrExecutionCtx, this.notFoundHandler); | ||
const c = new context_1.HonoContext(request, env, eventOrExecutionCtx, this.notFoundHandler); | ||
const composed = (0, compose_1.compose)(handlers, this.errorHandler, this.notFoundHandler); | ||
@@ -106,0 +106,0 @@ let context; |
/// <reference path="request.d.ts" /> | ||
import { Hono } from './hono'; | ||
export type { Handler, Next } from './hono'; | ||
export { Context } from './context'; | ||
export type { Context } from './context'; | ||
declare module './hono' { | ||
@@ -6,0 +6,0 @@ interface Hono { |
"use strict"; | ||
// @denoify-ignore | ||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference | ||
/// <reference path="./request.ts" /> Import "declare global" for the Request interface. | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Hono = exports.Context = void 0; | ||
exports.Hono = void 0; | ||
const hono_1 = require("./hono"); | ||
Object.defineProperty(exports, "Hono", { enumerable: true, get: function () { return hono_1.Hono; } }); | ||
var context_1 = require("./context"); | ||
Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return context_1.Context; } }); | ||
hono_1.Hono.prototype.fire = function () { | ||
@@ -11,0 +10,0 @@ addEventListener('fetch', (event) => { |
@@ -9,11 +9,2 @@ "use strict"; | ||
const auth = (req) => { | ||
if (!req) { | ||
throw new TypeError('argument req is required'); | ||
} | ||
if (typeof req !== 'object') { | ||
throw new TypeError('argument req is required to be an object'); | ||
} | ||
if (!req.headers || typeof req.headers !== 'object') { | ||
throw new TypeError('argument req is required to have headers property'); | ||
} | ||
const match = CREDENTIALS_REGEXP.exec(req.headers.get('Authorization') || ''); | ||
@@ -20,0 +11,0 @@ if (!match) { |
@@ -5,2 +5,19 @@ "use strict"; | ||
const html_1 = require("../../utils/html"); | ||
const emptyTags = [ | ||
'area', | ||
'base', | ||
'br', | ||
'col', | ||
'embed', | ||
'hr', | ||
'img', | ||
'input', | ||
'keygen', | ||
'link', | ||
'meta', | ||
'param', | ||
'source', | ||
'track', | ||
'wbr', | ||
]; | ||
const jsx = (tag, props, ...children) => { | ||
@@ -29,2 +46,5 @@ if (typeof tag === 'function') { | ||
if (tag !== '') { | ||
if (emptyTags.includes(tag)) { | ||
result += '/'; | ||
} | ||
result += '>'; | ||
@@ -45,3 +65,3 @@ } | ||
} | ||
if (tag !== '') { | ||
if (tag !== '' && !emptyTags.includes(tag)) { | ||
result += `</${tag}>`; | ||
@@ -48,0 +68,0 @@ } |
@@ -9,2 +9,5 @@ "use strict"; | ||
} | ||
if (!crypto.subtle || !crypto.subtle.importKey) { | ||
throw new Error('`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.'); | ||
} | ||
return async (ctx, next) => { | ||
@@ -11,0 +14,0 @@ const credentials = ctx.req.headers.get('Authorization'); |
@@ -0,1 +1,2 @@ | ||
import type { Cookie } from './utils/cookie'; | ||
declare global { | ||
@@ -20,4 +21,12 @@ interface Request<ParamKeyType extends string = string> { | ||
}; | ||
cookie: { | ||
(name: string): string; | ||
(): Cookie; | ||
}; | ||
parsedBody?: Promise<any>; | ||
parseBody: { | ||
(): Promise<any>; | ||
}; | ||
} | ||
} | ||
export declare function extendRequestPrototype(): void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.extendRequestPrototype = void 0; | ||
const body_1 = require("./utils/body"); | ||
const cookie_1 = require("./utils/cookie"); | ||
function extendRequestPrototype() { | ||
@@ -58,3 +60,20 @@ if (!!Request.prototype.param) { | ||
}; | ||
Request.prototype.cookie = function (key) { | ||
const cookie = this.headers.get('Cookie') || ''; | ||
const obj = (0, cookie_1.parse)(cookie); | ||
if (key) { | ||
const value = obj[key]; | ||
return value; | ||
} | ||
else { | ||
return obj; | ||
} | ||
}; | ||
Request.prototype.parseBody = function () { | ||
if (!this.parsedBody) { | ||
this.parsedBody = (0, body_1.parseBody)(this); | ||
} | ||
return this.parsedBody; | ||
}; | ||
} | ||
exports.extendRequestPrototype = extendRequestPrototype; |
@@ -53,2 +53,5 @@ "use strict"; | ||
const signing = async (data, secret, alg = types_1.AlgorithmTypes.HS256) => { | ||
if (!crypto.subtle || !crypto.subtle.importKey) { | ||
throw new Error('`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.'); | ||
} | ||
const cryptoKey = await crypto.subtle.importKey(CryptoKeyFormat.RAW, (0, encode_1.utf8ToUint8Array)(secret), param(alg), false, [CryptoKeyUsage.Sign]); | ||
@@ -55,0 +58,0 @@ return await crypto.subtle.sign(param(alg), cryptoKey, (0, encode_1.utf8ToUint8Array)(data)); |
{ | ||
"name": "hono", | ||
"version": "1.6.4", | ||
"version": "2.0.0", | ||
"description": "Ultrafast web framework for Cloudflare Workers.", | ||
@@ -12,2 +12,4 @@ "main": "dist/index.js", | ||
"test": "jest", | ||
"test:deno": "deno test --allow-read deno_test", | ||
"test:bun": "bun run ./bun_test/index.test.ts", | ||
"lint": "eslint --ext js,ts src .eslintrc.js", | ||
@@ -18,3 +20,3 @@ "lint:fix": "eslint --ext js,ts src .eslintrc.js --fix", | ||
"watch": "tsc --project tsconfig.build.json -w", | ||
"prerelease": "yarn denoify && yarn build", | ||
"prerelease": "yarn denoify && yarn test:deno && yarn build", | ||
"release": "np" | ||
@@ -26,7 +28,4 @@ }, | ||
"./bearer-auth": "./dist/middleware/bearer-auth/index.js", | ||
"./body-parse": "./dist/middleware/body-parse/index.js", | ||
"./cookie": "./dist/middleware/cookie/index.js", | ||
"./cors": "./dist/middleware/cors/index.js", | ||
"./etag": "./dist/middleware/etag/index.js", | ||
"./graphql-server": "./dist/middleware/graphql-server/index.js", | ||
"./html": "./dist/middleware/html/index.js", | ||
@@ -36,7 +35,6 @@ "./jsx": "./dist/middleware/jsx/index.js", | ||
"./logger": "./dist/middleware/logger/index.js", | ||
"./mustache": "./dist/middleware/mustache/index.js", | ||
"./mustache.module": "./dist/middleware/mustache/module.mjs", | ||
"./powered-by": "./dist/middleware/powered-by/index.js", | ||
"./pretty-json": "./dist/middleware/pretty-json/index.js", | ||
"./serve-static": "./dist/middleware/serve-static/index.js", | ||
"./serve-static.bun": "./dist/middleware/serve-static/bun.js", | ||
"./serve-static.module": "./dist/middleware/serve-static/module.mjs", | ||
@@ -56,5 +54,2 @@ "./router/trie-router": "./dist/router/trie-router/index.js", | ||
], | ||
"body-parse": [ | ||
"./dist/middleware/body-parse" | ||
], | ||
"cookie": [ | ||
@@ -69,5 +64,2 @@ "./dist/middleware/cookie" | ||
], | ||
"graphql-server": [ | ||
"./dist/middleware/graphql-server" | ||
], | ||
"html": [ | ||
@@ -85,8 +77,2 @@ "./dist/middleware/html" | ||
], | ||
"mustache": [ | ||
"./dist/middleware/mustache" | ||
], | ||
"mustache.module": [ | ||
"./dist/middleware/mustache/module.d.mts" | ||
], | ||
"powered-by": [ | ||
@@ -101,2 +87,5 @@ "./dist/middleware/powered-by" | ||
], | ||
"serve-static.bun": [ | ||
"./dist/middleware/serve-static/bun.d.ts" | ||
], | ||
"serve-static.module": [ | ||
@@ -130,2 +119,3 @@ "./dist/middleware/serve-static/module.d.mts" | ||
"keywords": [ | ||
"hono", | ||
"web", | ||
@@ -140,3 +130,5 @@ "app", | ||
"fastly", | ||
"compute@edge" | ||
"compute@edge", | ||
"deno", | ||
"bun" | ||
], | ||
@@ -162,9 +154,7 @@ "devDependencies": { | ||
"form-data": "^4.0.0", | ||
"graphql": "^16.4.0", | ||
"jest": "27.5.1", | ||
"jest-environment-miniflare": "^2.5.1", | ||
"jest-environment-miniflare": "^2.6.0", | ||
"mustache": "^4.2.0", | ||
"np": "^7.6.2", | ||
"prettier": "^2.6.2", | ||
"prettier-plugin-md-nocjsp": "^1.2.0", | ||
"rimraf": "^3.0.2", | ||
@@ -171,0 +161,0 @@ "ts-jest": "^27.1.4", |
785
README.md
<div align="center"> | ||
<a href="https://github.com/honojs/hono"> | ||
<a href="https://honojs.dev"> | ||
<img src="https://raw.githubusercontent.com/honojs/hono/master/docs/images/hono-title.png" width="500" height="auto" alt="Hono"/> | ||
@@ -9,2 +9,9 @@ </a> | ||
<p align="center"> | ||
<a href="https://honojs.dev"><b>Documentation :point_right: honojs.dev</b></a><br /> | ||
<i>v2.x has been released!</i> <a href="docs/MIGRATION.md">Migration guide</b> | ||
</p> | ||
<hr /> | ||
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/honojs/hono/ci)](https://github.com/honojs/hono/actions) | ||
@@ -27,3 +34,3 @@ [![GitHub](https://img.shields.io/github/license/honojs/hono)](https://github.com/honojs/hono/blob/master/LICENSE) | ||
app.fire() | ||
export default app | ||
``` | ||
@@ -35,3 +42,3 @@ | ||
- **Zero-dependencies** - using only Service Worker and Web Standard API. | ||
- **Middleware** - built-in middleware and ability to extend with your own middleware. | ||
- **Middleware** - built-in middleware, custom middleware, and third-party middleware. | ||
- **TypeScript** - first-class TypeScript support. | ||
@@ -42,7 +49,2 @@ - **Multi-platform** - works on Cloudflare Workers, Fastly Compute@Edge, Deno, or Bun. | ||
### Cloudflare Workers | ||
- Machine: Apple MacBook Pro, 32 GiB, M1 Pro | ||
- Scripts: [benchmarks/handle-event](https://github.com/honojs/hono/tree/master/benchmarks/handle-event) | ||
**Hono is fastest**, compared to other routers for Cloudflare Workers. | ||
@@ -60,759 +62,10 @@ | ||
### Deno | ||
## Documentation | ||
- Machine: Apple MacBook Pro, 32 GiB, M1 Pro, Deno v1.22.0 | ||
- Scripts: [benchmarks/deno](https://github.com/honojs/hono/tree/master/benchmarks/deno) | ||
- Method: `autocannon -c 100 -d 40 -p 10 'http://127.0.0.1:8000/user/lookup/username/foo'` | ||
The documentation is available on [honojs.dev](https://honojs.dev). | ||
**Hono is fastest**, compared to other frameworks for Deno. | ||
## Migration | ||
| Framework | Version | Results | | ||
| ----------------------------- | :-----: | ----------------------------------------: | | ||
| **Hono - RegExpRouter** | 1.6.0 | **5118k requests in 40.02s, 865 MB read** | | ||
| **Hono - TriRouter(default)** | 1.6.0 | **4932k requests in 40.02s, 833 MB read** | | ||
| Faster | 5.7 | 3579k requests in 40.02s, 551 MB read | | ||
| oak | 10.5.1 | 2385k requests in 40.02s, 403 MB read | | ||
| opine | 2.2.0 | 1491k requests in 40.02s, 346 MB read | | ||
Migration guide is available on [docs/MIGRATION.md](docs/MIGRATION.md) | ||
Another benchmark result: [denosaurs/bench](https://github.com/denosaurs/bench) | ||
## Why so fast? | ||
Routers used in Hono are really smart. | ||
- **TrieRouter**(default) - Implemented with Trie tree structure. | ||
- **RegExpRouter** - Match the route with using one big Regex made before dispatch. | ||
## Hono in 1 minute | ||
A demonstration to create an application for Cloudflare Workers with Hono. | ||
![Demo](https://user-images.githubusercontent.com/10682/151973526-342644f9-71c5-4fee-81f4-64a7558bb192.gif) | ||
## Not only fast | ||
Hono is fast. But not only fast. | ||
### Write Less, do more | ||
Built-in middleware make _"**Write Less, do more**"_ in reality. You can use a lot of middleware without writing code from scratch. Below are examples. | ||
- [Basic Authentication](https://github.com/honojs/hono/tree/master/src/middleware/basic-auth/) | ||
- [Cookie parsing / serializing](https://github.com/honojs/hono/tree/master/src/middleware/cookie/) | ||
- [CORS](https://github.com/honojs/hono/tree/master/src/middleware/cors/) | ||
- [ETag](https://github.com/honojs/hono/tree/master/src/middleware/etag/) | ||
- [GraphQL Server](https://github.com/honojs/hono/tree/master/src/middleware/graphql-server/) | ||
- [html](https://github.com/honojs/hono/tree/master/src/middleware/html/) | ||
- [JSX](https://github.com/honojs/hono/tree/master/src/middleware/jsx/) | ||
- [JWT Authentication](https://github.com/honojs/hono/tree/master/src/middleware/jwt/) | ||
- [Logger](https://github.com/honojs/hono/tree/master/src/middleware/logger/) | ||
- [Mustache template engine](https://github.com/honojs/hono/tree/master/src/middleware/mustache/) (Only for Cloudflare Workers) | ||
- [JSON pretty printing](https://github.com/honojs/hono/tree/master/src/middleware/pretty-json/) | ||
- [Serving static files](https://github.com/honojs/hono/tree/master/src/middleware/serve-static/) (Only for Cloudflare Workers and Deno) | ||
To enable logger and Etag middleware with just this code. | ||
```ts | ||
import { Hono } from 'hono' | ||
import { etag } from 'hono/etag' | ||
import { logger } from 'hono/logger' | ||
const app = new Hono() | ||
app.use('*', etag(), logger()) | ||
``` | ||
And, the routing of Hono is so flexible. It's easy to construct large web applications. | ||
```ts | ||
import { Hono } from 'hono' | ||
import { basicAuth } from 'hono/basic-auth' | ||
const v1 = new Hono() | ||
v1.get('/posts', (c) => { | ||
return c.text('list posts') | ||
}) | ||
.post(basicAuth({ username, password }), (c) => { | ||
return c.text('created!', 201) | ||
}) | ||
.get('/posts/:id', (c) => { | ||
const id = c.req.param('id') | ||
return c.text(`your id is ${id}`) | ||
}) | ||
const app = new Hono() | ||
app.route('/v1', v1) | ||
``` | ||
### Web Standard | ||
Request and Response object used in Hono are extensions of the Web Standard [Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API). If you are familiar with that, you don't need to know more than that. | ||
### Developer Experience | ||
Hono provides fine _"**Developer Experience**"_. Easy access to Request/Response thanks to the `Context` object. | ||
Above all, Hono is written in TypeScript. So, Hono has _"**Types**"_! | ||
For example, the named path parameters will be literal types. | ||
![Demo](https://user-images.githubusercontent.com/10682/154179671-9e491597-6778-44ac-a8e6-4483d7ad5393.png) | ||
## Install | ||
You can install Hono from the npm registry. | ||
```sh | ||
npm install hono | ||
``` | ||
## Methods | ||
An instance of `Hono` has these methods. | ||
- app.**HTTP_METHOD**(\[path,\]handler|middleware...) | ||
- app.**all**(\[path,\]handler|middleware...) | ||
- app.**route**(path, \[app\]) | ||
- app.**use**(\[path,\]middleware) | ||
- app.**notFound**(handler) | ||
- app.**onError**(err, handler) | ||
- app.**fire**() | ||
- app.**fetch**(request, env, event) | ||
- app.**request**(path, options) | ||
## Routing | ||
### Basic | ||
```ts | ||
// HTTP Methods | ||
app.get('/', (c) => c.text('GET /')) | ||
app.post('/', (c) => c.text('POST /')) | ||
app.put('/', (c) => c.text('PUT /')) | ||
app.delete('/', (c) => c.text('DELETE /')) | ||
// Wildcard | ||
app.get('/wild/*/card', (c) => { | ||
return c.text('GET /wild/*/card') | ||
}) | ||
// Any HTTP methods | ||
app.all('/hello', (c) => c.text('Any Method /hello')) | ||
``` | ||
### Named Parameter | ||
```ts | ||
app.get('/user/:name', (c) => { | ||
const name = c.req.param('name') | ||
... | ||
}) | ||
``` | ||
or all parameters at once: | ||
```ts | ||
app.get('/posts/:id/comment/:comment_id', (c) => { | ||
const { id, comment_id } = c.req.param() | ||
... | ||
}) | ||
``` | ||
### Regexp | ||
```ts | ||
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => { | ||
const { date, title } = c.req.param() | ||
... | ||
}) | ||
``` | ||
### Chained route | ||
```ts | ||
app | ||
.get('/endpoint', (c) => { | ||
return c.text('GET /endpoint') | ||
}) | ||
.post((c) => { | ||
return c.text('POST /endpoint') | ||
}) | ||
.delete((c) => { | ||
return c.text('DELETE /endpoint') | ||
}) | ||
``` | ||
### no strict | ||
If `strict` is set false, `/hello`and`/hello/` are treated the same. | ||
```ts | ||
const app = new Hono({ strict: false }) // Default is true | ||
app.get('/hello', (c) => c.text('/hello or /hello/')) | ||
``` | ||
### async/await | ||
```js | ||
app.get('/fetch-url', async (c) => { | ||
const response = await fetch('https://example.com/') | ||
return c.text(`Status is ${response.status}`) | ||
}) | ||
``` | ||
## Grouping | ||
Group the routes with `Hono` instance and add them to the main app with `route` method. | ||
```ts | ||
const book = new Hono() | ||
book.get('/', (c) => c.text('List Books')) // GET /book | ||
book.get('/:id', (c) => { | ||
// GET /book/:id | ||
const id = c.req.param('id') | ||
return c.text('Get Book: ' + id) | ||
}) | ||
book.post('/', (c) => c.text('Create Book')) // POST /book | ||
const app = new Hono() | ||
app.route('/book', book) | ||
``` | ||
## Middleware | ||
Middleware works after/before Handler. We can get `Request` before dispatching or manipulate `Response` after dispatching. | ||
### Definition of Middleware | ||
- Handler - should return `Response` object. Only one handler will be called. | ||
- Middleware - should return nothing, will be proceeded to next middleware with `await next()` | ||
The user can register middleware using `c.use` or using `c.HTTP_METHOD` as well as the handlers. For this feature, it's easy to specify the path and the method. | ||
```ts | ||
// match any method, all routes | ||
app.use('*', logger()) | ||
// specify path | ||
app.use('/posts/*', cors()) | ||
// specify method and path | ||
app.post('/posts/*', basicAuth(), bodyParse()) | ||
``` | ||
If the handler returns `Response`, it will be used for the end-user, and stopping the processing. | ||
```ts | ||
app.post('/posts', (c) => c.text('Created!', 201)) | ||
``` | ||
In this case, four middleware are processed before dispatching like this: | ||
```ts | ||
logger() -> cors() -> basicAuth() -> bodyParse() -> *handler* | ||
``` | ||
### Built-in Middleware | ||
Hono has built-in middleware. | ||
```ts | ||
import { Hono } from 'hono' | ||
import { poweredBy } from 'hono/powered-by' | ||
import { logger } from 'hono/logger' | ||
import { basicAuth } from 'hono/basicAuth' | ||
const app = new Hono() | ||
app.use('*', poweredBy()) | ||
app.use('*', logger()) | ||
app.use( | ||
'/auth/*', | ||
basicAuth({ | ||
username: 'hono', | ||
password: 'acoolproject', | ||
}) | ||
) | ||
``` | ||
Available built-in middleware is listed on [src/middleware](https://github.com/honojs/hono/tree/master/src/middleware). | ||
### Custom Middleware | ||
You can write your own middleware. | ||
```ts | ||
// Custom logger | ||
app.use('*', async (c, next) => { | ||
console.log(`[${c.req.method}] ${c.req.url}`) | ||
await next() | ||
}) | ||
// Add a custom header | ||
app.use('/message/*', async (c, next) => { | ||
await next() | ||
c.header('x-message', 'This is middleware!') | ||
}) | ||
app.get('/message/hello', (c) => c.text('Hello Middleware!')) | ||
``` | ||
## Not Found | ||
`app.notFound` for customizing Not Found Response. | ||
```js | ||
app.notFound((c) => { | ||
return c.text('Custom 404 Message', 404) | ||
}) | ||
``` | ||
## Error Handling | ||
`app.onError` handle the error and return the customized Response. | ||
```js | ||
app.onError((err, c) => { | ||
console.error(`${err}`) | ||
return c.text('Custom Error Message', 500) | ||
}) | ||
``` | ||
## Context | ||
To handle Request and Response, you can use `Context` object. | ||
### c.req | ||
```ts | ||
// Get Request object | ||
app.get('/hello', (c) => { | ||
const userAgent = c.req.headers.get('User-Agent') | ||
... | ||
}) | ||
// Shortcut to get a header value | ||
app.get('/shortcut', (c) => { | ||
const userAgent = c.req.header('User-Agent') | ||
... | ||
}) | ||
// 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() | ||
... | ||
}) | ||
// Multiple query values | ||
app.get('/search', (c) => { | ||
const queries = c.req.queries('q') | ||
// ---> GET search?q=foo&q=bar | ||
// queries[0] => foo, queries[1] => bar | ||
... | ||
}) | ||
// Captured params | ||
app.get('/entry/:id', (c) => { | ||
const id = c.req.param('id') | ||
... | ||
}) | ||
``` | ||
### Shortcuts for Response | ||
```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 comming') | ||
}) | ||
``` | ||
The Response is the same as below. | ||
```ts | ||
new Response('Thank you for comming', { | ||
status: 201, | ||
headers: { | ||
'X-Message': 'Hello', | ||
'Content-Type': 'text/plain', | ||
}, | ||
}) | ||
``` | ||
### c.text() | ||
Render text as `Content-Type:text/plain`. | ||
```ts | ||
app.get('/say', (c) => { | ||
return c.text('Hello!') | ||
}) | ||
``` | ||
### c.json() | ||
Render JSON as `Content-Type:application/json`. | ||
```ts | ||
app.get('/api', (c) => { | ||
return c.json({ message: 'Hello!' }) | ||
}) | ||
``` | ||
### c.html() | ||
Render HTML as `Content-Type:text/html`. | ||
```ts | ||
app.get('/', (c) => { | ||
return c.html('<h1>Hello! Hono!</h1>') | ||
}) | ||
``` | ||
### c.notFound() | ||
Return the `Not Found` Response. | ||
```ts | ||
app.get('/notfound', (c) => { | ||
return c.notFound() | ||
}) | ||
``` | ||
### c.redirect() | ||
Redirect, default status code is `302`. | ||
```ts | ||
app.get('/redirect', (c) => c.redirect('/')) | ||
app.get('/redirect-permanently', (c) => c.redirect('/', 301)) | ||
``` | ||
### c.res | ||
```ts | ||
// Response object | ||
app.use('/', async (c, next) => { | ||
await next() | ||
c.res.headers.append('X-Debug', 'Debug message') | ||
}) | ||
``` | ||
### c.executionCtx | ||
```ts | ||
// ExecutionContext object | ||
app.get('/foo', async (c) => { | ||
c.executionCtx.waitUntil( | ||
c.env.KV.put(key, data) | ||
) | ||
... | ||
}) | ||
``` | ||
### c.event | ||
```ts | ||
// FetchEvent object (only set when using Service Worker syntax) | ||
app.get('/foo', async (c) => { | ||
c.event.waitUntil( | ||
c.env.KV.put(key, data) | ||
) | ||
... | ||
}) | ||
``` | ||
### c.env | ||
Environment variables, secrets, and KV namespaces are known as bindings. Regardless of type, bindings are always available as global variables and can be accessed via the context `c.env.BINDING_KEY`. | ||
```ts | ||
// Environment object for Cloudflare Workers | ||
app.get('*', async c => { | ||
const counter = c.env.COUNTER | ||
... | ||
}) | ||
``` | ||
## fire | ||
`app.fire()` do this. | ||
```ts | ||
addEventListener('fetch', (event) => { | ||
event.respondWith(this.handleEvent(event)) | ||
}) | ||
``` | ||
## fetch | ||
`app.fetch` for Cloudflare Module Worker syntax. | ||
```ts | ||
export default { | ||
fetch(request: Request, env: Env, ctx: ExecutionContext) { | ||
return app.fetch(request, env, ctx) | ||
}, | ||
} | ||
``` | ||
or just do: | ||
```ts | ||
export default app | ||
``` | ||
## request | ||
`request` is a useful method for testing. | ||
```js | ||
test('GET /hello is ok', async () => { | ||
const res = await app.request('http://localhost/hello') | ||
expect(res.status).toBe(200) | ||
}) | ||
``` | ||
## router | ||
The `router` option specify which router is used inside. The default router is `TrieRouter`. If you want to use `RexExpRouter`, write like this: | ||
```ts | ||
import { RegExpRouter } from 'hono/router/reg-exp-router' | ||
const app = new Hono({ router: new RegExpRouter() }) | ||
``` | ||
## Routing priority | ||
Handlers or middleware will be executed in registration order. | ||
```ts | ||
app.get('/book/a', (c) => c.text('a')) // a | ||
app.get('/book/:slug', (c) => c.text('common')) // common | ||
``` | ||
```http | ||
GET /book/a ---> `a` | ||
GET /book/b ---> `common` | ||
``` | ||
When a handler is executed, the process will be stopped. | ||
```ts | ||
app.get('*', (c) => c.text('common')) // common | ||
app.get('/foo', (c) => c.text('foo')) // foo | ||
``` | ||
```http | ||
GET /foo ---> `common` // foo will not be dispatched | ||
``` | ||
If you have the middleware that you want to execute, write the code above the handler. | ||
```ts | ||
app.use('*', logger()) | ||
app.get('/foo', (c) => c.text('foo')) | ||
``` | ||
If you want a "_fallback_" handler, write the code below the other handler. | ||
```ts | ||
app.get('/foo', (c) => c.text('foo')) // foo | ||
app.get('*', (c) => c.text('fallback')) // fallback | ||
``` | ||
```http | ||
GET /bar ---> `fallback` | ||
``` | ||
## Cloudflare Workers with Hono | ||
Using [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/), you can develop the application locally and publish it with few commands. | ||
Let's write your first code for Cloudflare Workers with Hono. | ||
### 1. `wrangler init` | ||
Initialize as a wrangler project. | ||
``` | ||
mkdir hono-example | ||
cd hono-example | ||
npx wrangler init -y | ||
``` | ||
### 2. `npm install hono` | ||
Install `hono` from the npm registry. | ||
``` | ||
npm init -y | ||
npm i hono | ||
``` | ||
### 3. Write your app | ||
Edit `src/index.ts`. Only 4 lines!! | ||
```ts | ||
// src/index.ts | ||
import { Hono } from 'hono' | ||
const app = new Hono() | ||
app.get('/', (c) => c.text('Hello! Hono!')) | ||
app.fire() | ||
``` | ||
### 4. Run | ||
Run the development server locally. Then, access `http://127.0.0.1:8787/` in your Web browser. | ||
``` | ||
npx wrangler dev | ||
``` | ||
### 5. Publish | ||
Deploy to Cloudflare. That's all! | ||
``` | ||
npx wrangler publish ./src/index.ts | ||
``` | ||
## Starter template | ||
You can start making your Cloudflare Workers application with [the starter template](https://github.com/honojs/hono-minimal). It is really minimal using TypeScript, esbuild, Miniflare, and Jest. | ||
To generate a project skeleton, run this command. | ||
``` | ||
npx create-cloudflare my-app https://github.com/honojs/hono-minimal | ||
``` | ||
## Practical Example | ||
How about writing web API with Hono? | ||
```ts | ||
import { Hono } from 'hono' | ||
import { cors } from 'hono/cors' | ||
import { basicAuth } from 'hono/basic-auth' | ||
import { prettyJSON } from 'hono/pretty-json' | ||
import { getPosts, getPost, createPost, Post } from './model' | ||
const app = new Hono() | ||
app.get('/', (c) => c.text('Pretty Blog API')) | ||
app.use('*', prettyJSON()) | ||
app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404)) | ||
export interface Bindings { | ||
USERNAME: string | ||
PASSWORD: string | ||
} | ||
const api = new Hono<Bindings>() | ||
api.use('/posts/*', cors()) | ||
api.get('/posts', (c) => { | ||
const { limit, offset } = c.req.query() | ||
const posts = getPosts({ limit, offset }) | ||
return c.json({ posts }) | ||
}) | ||
api.get('/posts/:id', (c) => { | ||
const id = c.req.param('id') | ||
const post = getPost({ id }) | ||
return c.json({ post }) | ||
}) | ||
api.post( | ||
'/posts', | ||
async (c, next) => { | ||
const auth = basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD }) | ||
await auth(c, next) | ||
}, | ||
async (c) => { | ||
const post = await c.req.json<Post>() | ||
const ok = createPost({ post }) | ||
return c.json({ ok }) | ||
} | ||
) | ||
app.route('/api', api) | ||
export default app | ||
``` | ||
## Other Examples | ||
- Hono Examples - <https://github.com/honojs/examples> | ||
## Deno | ||
Hono also works on Deno. This feature is still experimental. | ||
```tsx | ||
/** @jsx jsx */ | ||
import { serve } from 'https://deno.land/std/http/server.ts' | ||
import { Hono, logger, poweredBy, serveStatic, jsx } from 'https://deno.land/x/hono/mod.ts' | ||
const app = new Hono() | ||
app.use('*', logger(), poweredBy()) | ||
app.get('/favicon.ico', serveStatic({ path: './public/favicon.ico' })) | ||
app.get('/', (c) => { | ||
return c.html(<h1>Hello Deno!</h1>) | ||
}) | ||
serve(app.fetch) | ||
``` | ||
## Bun | ||
Hono also works on Bun. This feature is still experimental. | ||
```ts | ||
import { Hono } from 'hono' | ||
const app = new Hono() | ||
app.get('/', (c) => { | ||
return c.json({ message: 'Hello Bun!' }) | ||
}) | ||
export default { | ||
port: 3000, | ||
fetch: app.fetch, | ||
} | ||
``` | ||
## Related projects | ||
Implementation of the original router `TrieRouter` is inspired by [goblin](https://github.com/bmf-san/goblin). `RegExpRouter` is inspired by [Router::Boom](https://github.com/tokuhirom/Router-Boom). API design is inspired by [express](https://github.com/expressjs/express) and [koa](https://github.com/koajs/koa). [itty-router](https://github.com/kwhitley/itty-router), [Sunder](https://github.com/SunderJS/sunder), and [worktop](https://github.com/lukeed/worktop) are the other routers or frameworks for Cloudflare Workers. | ||
- express - <https://github.com/expressjs/express> | ||
- koa - <https://github.com/koajs/koa> | ||
- itty-router - <https://github.com/kwhitley/itty-router> | ||
- Sunder - <https://github.com/SunderJS/sunder> | ||
- goblin - <https://github.com/bmf-san/goblin> | ||
- worktop - <https://github.com/lukeed/worktop> | ||
- Router::Boom - <https://github.com/tokuhirom/Router-Boom> | ||
## Contributing | ||
@@ -822,6 +75,8 @@ | ||
- Write or fix documents | ||
- Write code of middleware | ||
- Fix bugs | ||
- Refactor the code | ||
- Fix bugs. | ||
- Create built-in or third-party middleware. | ||
- Propose new feature. | ||
- Refactor the code. | ||
- Write an article about Hono on your Blog. | ||
- Fix a typo. | ||
- etc. | ||
@@ -828,0 +83,0 @@ |
26
109240
87
2785
89