diff --git a/README.md b/README.md index bf3e268f..b407c4a1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Fastest is hono Below is a demonstration to create an application of Cloudflare Workers with Hono. -![Demo](https://user-images.githubusercontent.com/10682/148223268-2484a891-57c1-472f-9df3-936a5586f002.gif) +![Demo](https://user-images.githubusercontent.com/10682/151102477-be0f950e-8d23-49c5-b6d8-d8ecb6b7484e.gif) ## Install @@ -90,7 +90,7 @@ app.all('/hello', (c) => c.text('Any Method /hello')) ```js app.get('/user/:name', (c) => { - const name = c.req.params('name') + const name = c.req.param('name') ... }) ``` @@ -99,8 +99,8 @@ app.get('/user/:name', (c) => { ```js app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => { - const date = c.req.params('date') - const title = c.req.params('title') + const date = c.req.param('date') + const title = c.req.param('title') ... ``` @@ -219,13 +219,18 @@ To handle Request and Reponse easily, you can use Context object: ### c.req ```js - // 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') @@ -234,40 +239,31 @@ app.get('/search', (c) => { // Captured params app.get('/entry/:id', (c) => { - const id = c.req.params('id') + const id = c.req.param('id') ... }) ``` -### c.res +### Shortcuts for Response ```js -// Response object -app.use('/', (c, next) => { - next() - c.res.headers.append('X-Debug', 'Debug message') -}) -``` - -### c.event - -```js -// FetchEvent object -app.use('*', async (c, next) => { - c.event.waitUntil( - ... - ) - await next() -}) -``` - -### c.env - -```js -// Environment object for Cloudflare Workers -app.get('*', async c => { - const counter = c.env.COUNTER - ... +app.get('/welcome', (c) => { + c.header('X-Message', 'Hello!') + c.header('Content-Type', 'text/plain') + c.status(201) + c.statusText('201 Content Created') + return c.body('Thank you for comming') + /* + Same as: + return new Response('Thank you for comming', { + status: 201, + statusText: '201 Content Created', + headers: { + 'X-Message': 'Hello', + 'Content-Type': 'text/plain' + } + }) + */ }) ``` @@ -310,6 +306,38 @@ app.get('/redirect', (c) => c.redirect('/')) app.get('/redirect-permanently', (c) => c.redirect('/', 301)) ``` +### c.res + +```js +// Response object +app.use('/', (c, next) => { + next() + c.res.headers.append('X-Debug', 'Debug message') +}) +``` + +### c.event + +```js +// FetchEvent object +app.use('*', async (c, next) => { + c.event.waitUntil( + ... + ) + await next() +}) +``` + +### c.env + +```js +// Environment object for Cloudflare Workers +app.get('*', async c => { + const counter = c.env.COUNTER + ... +}) +``` + ## fire `app.fire()` do: diff --git a/example/basic/index.js b/example/basic/index.js index ae88f10e..de8e0981 100644 --- a/example/basic/index.js +++ b/example/basic/index.js @@ -55,7 +55,7 @@ app.get('/hello', () => new Response('This is /hello')) // Named parameter app.get('/entry/:id', (c) => { - const id = c.req.params('id') + const id = c.req.param('id') return c.text(`Your ID is ${id}`) }) // Redirect @@ -71,7 +71,7 @@ app.get('/fetch-url', async (c) => { // Request headers app.get('/user-agent', (c) => { - const userAgent = c.req.headers.get('User-Agent') + const userAgent = c.req.header('User-Agent') return c.text(`Your UserAgent is ${userAgent}`) }) @@ -90,8 +90,8 @@ app.post('/api/posts', (c) => c.json({ message: 'Created!' }, 201)) // default route app.get('/api/*', (c) => c.text('API endpoint is not found', 404)) -app.post('/form', async (ctx) => { - return ctx.json(ctx.req.parsedBody || {}) +app.post('/form', async (c) => { + return c.json(c.req.parsedBody || {}) //return new Response('ok /form') }) diff --git a/example/blog/src/controller.ts b/example/blog/src/controller.ts index 77e3482b..5ef9b352 100644 --- a/example/blog/src/controller.ts +++ b/example/blog/src/controller.ts @@ -24,7 +24,7 @@ export const create: Handler = async (c) => { } export const show: Handler = async (c) => { - const id = c.req.params('id') + const id = c.req.param('id') const post = await Model.getPost(id) if (!post) { return c.json({ error: 'Not Found', ok: false }, 404) @@ -33,7 +33,7 @@ export const show: Handler = async (c) => { } export const update: Handler = async (c) => { - const id = c.req.params('id') + const id = c.req.param('id') if (!(await Model.getPost(id))) { // 204 No Content return c.json({ ok: false }, 204) @@ -44,7 +44,7 @@ export const update: Handler = async (c) => { } export const destroy: Handler = async (c) => { - const id = c.req.params('id') + const id = c.req.param('id') if (!(await Model.getPost(id))) { // 204 No Content return c.json({ ok: false }, 204) diff --git a/example/compute-at-edge/index.js b/example/compute-at-edge/index.js index 401d5f25..61f4b07b 100644 --- a/example/compute-at-edge/index.js +++ b/example/compute-at-edge/index.js @@ -10,7 +10,7 @@ app.use('*', async (c, next) => { app.get('/', (c) => c.text('Hono!! Compute@Edge!!')) app.get('/hello/:name', (c) => { - return c.text(`Hello ${c.req.params('name')}!!!!!`) + return c.text(`Hello ${c.req.param('name')}!!!!!`) }) app.fire() diff --git a/example/jsx-ssr/src/index.js b/example/jsx-ssr/src/index.js index 3646bdf1..3ef0ca93 100644 --- a/example/jsx-ssr/src/index.js +++ b/example/jsx-ssr/src/index.js @@ -48,7 +48,7 @@ app.get('/', (c) => { }) app.get('/post/:id{[0-9]+}', (c) => { - const id = c.req.params('id') + const id = c.req.param('id') const post = getPost(id) if (!post) { return c.text('Not Found', 404) diff --git a/src/context.ts b/src/context.ts index 2c062518..8b943a08 100644 --- a/src/context.ts +++ b/src/context.ts @@ -9,6 +9,10 @@ export class Context { res: Response env: Env event: FetchEvent + private _headers: Headers + private _status: number + private _statusText: string + body: (body: BodyInit, init?: ResponseInit) => Response constructor(req: Request, opts?: { res: Response; env: Env; event: FetchEvent }) { this.req = req @@ -17,9 +21,26 @@ export class Context { this.env = opts.env this.event = opts.event } + this._headers = {} + this.body = this.newResponse } - newResponse(body?: BodyInit | null | undefined, init?: ResponseInit | undefined): Response { + header(name: string, value: string): void { + this._headers[name] = value + } + + status(number: number): void { + this._status = number + } + + statusText(text: string): void { + this._statusText = text + } + + newResponse(body: BodyInit, init: ResponseInit = {}): Response { + init.status = this._status || init.status + init.statusText = this._statusText || init.statusText + init.headers = { ...init.headers, ...this._headers } return new Response(body, init) } diff --git a/src/hono.ts b/src/hono.ts index b2ae8c4a..144e174f 100644 --- a/src/hono.ts +++ b/src/hono.ts @@ -10,8 +10,9 @@ const METHOD_NAME_OF_ALL = 'ALL' declare global { interface Request { - params: (key: string) => string + param: (key: string) => string query: (key: string) => string | null + header: (name: string) => string parsedBody: any } } @@ -126,7 +127,7 @@ export class Hono { const result = await this.matchRoute(method, path) - request.params = (key: string): string => { + request.param = (key: string): string => { if (result) { return result.params[key] } diff --git a/src/middleware/default.ts b/src/middleware/default.ts index e5422f83..c24f2284 100644 --- a/src/middleware/default.ts +++ b/src/middleware/default.ts @@ -6,6 +6,9 @@ export const defaultMiddleware = async (c: Context, next: Function) => { const url = new URL(c.req.url) return url.searchParams.get(key) } + c.req.header = (name: string): string => { + return c.req.headers.get(name) + } await next() diff --git a/src/middleware/logger/logger.test.ts b/src/middleware/logger/logger.test.ts index 72a6d295..ca119a77 100644 --- a/src/middleware/logger/logger.test.ts +++ b/src/middleware/logger/logger.test.ts @@ -18,7 +18,7 @@ describe('Logger by Middleware', () => { app.get('/empty', () => new Response('')) it('Log status 200 with empty body', async () => { - const req = new Request('http://localhost/short') + const req = new Request('http://localhost/empty') const res = await app.dispatch(req) expect(res).not.toBeNull() expect(res.status).toBe(200) @@ -44,7 +44,7 @@ describe('Logger by Middleware', () => { expect(log.endsWith(` ${longRandomString.length / 1024}kB`)).toBe(true) }) - it.only('Log status 404', async () => { + it('Log status 404', async () => { const msg = 'Default 404 Nout Found' app.notFound = () => { return new Response(msg, { status: 404 }) diff --git a/test/context.test.ts b/test/context.test.ts index be86404a..0e8a58b3 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -4,7 +4,7 @@ describe('Context', () => { const req = new Request('http://localhost/') const c = new Context(req) - it('c.text', async () => { + it('c.text()', async () => { const res = c.text('text in c', 201, { 'X-Custom': 'Message' }) expect(res.status).toBe(201) expect(res.headers.get('Content-Type')).toBe('text/plain') @@ -12,7 +12,7 @@ describe('Context', () => { expect(res.headers.get('X-Custom')).toBe('Message') }) - it('c.json', async () => { + it('c.json()', async () => { const res = c.json({ message: 'Hello' }, 201, { 'X-Custom': 'Message' }) expect(res.status).toBe(201) expect(res.headers.get('Content-Type')).toMatch('application/json') @@ -22,7 +22,7 @@ describe('Context', () => { expect(res.headers.get('X-Custom')).toBe('Message') }) - it('c.html', async () => { + it('c.html()', async () => { const res = c.html('

Hello! Hono!

', 201, { 'X-Custom': 'Message' }) expect(res.status).toBe(201) expect(res.headers.get('Content-Type')).toMatch('text/html') @@ -30,7 +30,7 @@ describe('Context', () => { expect(res.headers.get('X-Custom')).toBe('Message') }) - it('c.redirect', async () => { + it('c.redirect()', async () => { let res = c.redirect('/destination') expect(res.status).toBe(302) expect(res.headers.get('Location')).toMatch(/^https?:\/\/.+\/destination$/) @@ -38,4 +38,30 @@ describe('Context', () => { expect(res.status).toBe(302) expect(res.headers.get('Location')).toBe('https://example.com/destination') }) + + it('c.header()', async () => { + c.header('X-Foo', 'Bar') + const res = c.body('Hi') + const foo = res.headers.get('X-Foo') + expect(foo).toBe('Bar') + }) + + it('c.status() and c.statusText()', async () => { + c.status(201) + c.statusText('Created!!!!') + const res = c.body('Hi') + expect(res.status).toBe(201) + expect(res.statusText).toBe('Created!!!!') + }) + + it('Complext pattern', async () => { + c.status(404) + c.statusText('Hono is Not Found') + const res = c.json({ hono: 'great app' }) + expect(res.status).toBe(404) + expect(res.statusText).toBe('Hono is Not Found') + expect(res.headers.get('Content-Type')).toMatch(/json/) + const obj: { [key: string]: string } = await res.json() + expect(obj['hono']).toBe('great app') + }) }) diff --git a/test/hono.test.ts b/test/hono.test.ts index 435caecf..67724e18 100644 --- a/test/hono.test.ts +++ b/test/hono.test.ts @@ -2,16 +2,26 @@ import { Hono } from '../src/hono' describe('GET Request', () => { const app = new Hono() + app.get('/hello', () => { return new Response('hello', { status: 200, }) }) + + app.get('/hello-with-shortcuts', (c) => { + c.header('X-Custom', 'This is Hono') + c.statusText('Hono is created') + c.status(201) + return c.html('

Hono!!!

') + }) + app.notFound = () => { return new Response('not found', { status: 404, }) } + it('GET /hello is ok', async () => { const req = new Request('http://localhost/hello') const res = await app.dispatch(req) @@ -19,6 +29,18 @@ describe('GET Request', () => { expect(res.status).toBe(200) expect(await res.text()).toBe('hello') }) + + it('GET /hell-with-shortcuts is ok', async () => { + const req = new Request('http://localhost/hello-with-shortcuts') + const res = await app.dispatch(req) + expect(res).not.toBeNull() + expect(res.status).toBe(201) + expect(res.statusText).toBe('Hono is created') + expect(res.headers.get('X-Custom')).toBe('This is Hono') + expect(res.headers.get('Content-Type')).toMatch(/text\/html/) + expect(await res.text()).toBe('

Hono!!!

') + }) + it('GET / is not found', async () => { const req = new Request('http://localhost/') const res = await app.dispatch(req) @@ -84,35 +106,51 @@ describe('Routing', () => { }) }) -describe('params and query', () => { +describe('param and query', () => { const app = new Hono() - app.get('/entry/:id', async (c) => { - const id = await c.req.params('id') + app.get('/entry/:id', (c) => { + const id = c.req.param('id') return new Response(`id is ${id}`, { status: 200, }) }) - app.get('/search', async (c) => { - const name = await c.req.query('name') + app.get('/search', (c) => { + const name = c.req.query('name') return new Response(`name is ${name}`, { status: 200, }) }) - it('params of /entry/:id is found', async () => { + app.get('/add-header', (c) => { + const bar = c.req.header('X-Foo') + return new Response(`foo is ${bar}`, { + status: 200, + }) + }) + + it('param of /entry/:id is found', async () => { const req = new Request('http://localhost/entry/123') const res = await app.dispatch(req) expect(res.status).toBe(200) expect(await res.text()).toBe('id is 123') }) + it('query of /search?name=sam is found', async () => { const req = new Request('http://localhost/search?name=sam') const res = await app.dispatch(req) expect(res.status).toBe(200) expect(await res.text()).toBe('name is sam') }) + + it('/add-header header - X-Foo is Bar', async () => { + const req = new Request('http://localhost/add-header') + req.headers.append('X-Foo', 'Bar') + const res = await app.dispatch(req) + expect(res.status).toBe(200) + expect(await res.text()).toBe('foo is Bar') + }) }) describe('Middleware', () => { @@ -127,24 +165,24 @@ describe('Middleware', () => { // Apeend Custom Header app.use('*', async (c, next) => { await next() - await c.res.headers.append('x-custom', 'root') + c.res.headers.append('x-custom', 'root') }) app.use('/hello', async (c, next) => { await next() - await c.res.headers.append('x-message', 'custom-header') + c.res.headers.append('x-message', 'custom-header') }) app.use('/hello/*', async (c, next) => { await next() - await c.res.headers.append('x-message-2', 'custom-header-2') + c.res.headers.append('x-message-2', 'custom-header-2') }) app.get('/hello', () => { return new Response('hello') }) app.get('/hello/:message', (c) => { - const message = c.req.params('message') + const message = c.req.param('message') return new Response(`${message}`) }) @@ -153,18 +191,18 @@ describe('Middleware', () => { const res = await app.dispatch(req) expect(res.status).toBe(200) expect(await res.text()).toBe('hello') - expect(await res.headers.get('x-custom')).toBe('root') - expect(await res.headers.get('x-message')).toBe('custom-header') - expect(await res.headers.get('x-message-2')).toBe('custom-header-2') + expect(res.headers.get('x-custom')).toBe('root') + expect(res.headers.get('x-message')).toBe('custom-header') + expect(res.headers.get('x-message-2')).toBe('custom-header-2') }) - it('logging and custom header with named params', async () => { + it('logging and custom header with named param', async () => { const req = new Request('http://localhost/hello/message') const res = await app.dispatch(req) expect(res.status).toBe(200) expect(await res.text()).toBe('message') - expect(await res.headers.get('x-custom')).toBe('root') - expect(await res.headers.get('x-message-2')).toBe('custom-header-2') + expect(res.headers.get('x-custom')).toBe('root') + expect(res.headers.get('x-message-2')).toBe('custom-header-2') }) })