Skip to content

Commit

Permalink
support res in handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
v1rtl committed Feb 22, 2021
1 parent 5e52689 commit 7bfa794
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 28 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Deno port of [tinyhttp](https://github.com/talentlessguy/tinyhttp), 0-legacy, tiny & fast web framework as a replacement of Express.

> **WARNING!** This port is very unstable and lacks features. It also doesn't have all of the tinyhttp's original extensions.
> **WARNING!** This port is very unstable and lacks features. It also doesn't have all of the tinyhttp's original extensions. Wait for the v2 release of tinyhttp for a better version (see [talentlessguy/tinyhttp#198](https://github.com/talentlessguy/tinyhttp/issues/198))
## Example

Expand All @@ -13,19 +13,17 @@ import { App } from 'https://deno.land/x/tinyhttp@v0.0.3/app.ts'

const app = new App()

app.use('/', (req, next) => {
app.use('/', (req, res, next) => {
console.log(`${req.method} ${req.url}`)

res.headers.set('Test-Header', 'Value')

next()
})

app.get('/:name/', (req) => {
req.respond({ body: `Hello ${req.params.name}!` })
app.get('/:name/', (req, res) => {
res.send(`Hello on ${req.url} from Deno and tinyhttp! 🦕`)
})

app.listen(3000, () => console.log(`Started on :3000`))
```

## Changes

Because Deno doesn't have the same API for HTTP server, there's no `res` argument. To send responses use `req.respond` instead.
39 changes: 24 additions & 15 deletions app.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
// deno-lint-ignore-file
import { NextFunction, Router, Handler, Middleware, UseMethodParams } from 'https://esm.sh/@tinyhttp/router'
import { NextFunction, Router, Handler, Middleware, UseMethodParams } from 'https://esm.sh/@tinyhttp/router@1.2.1'
import { onErrorHandler, ErrorHandler } from './onError.ts'
import 'https://deno.land/std@0.87.0/node/global.ts'
import rg from 'https://esm.sh/regexparam'
import { Request } from './request.ts'
import { getURLParams } from 'https://cdn.esm.sh/v15/@tinyhttp/url@1.1.2/esnext/url.js'
import { Response } from './response.ts'
import { getURLParams } from 'https://cdn.esm.sh/v15/@tinyhttp/url@1.2.0/esnext/url.js'
import { extendMiddleware } from './extend.ts'
import { serve, Server } from 'https://deno.land/std@0.87.0/http/server.ts'
import { parse } from './parseUrl.ts'

const lead = (x: string) => (x.charCodeAt(0) === 47 ? x : '/' + x)

export const applyHandler = <Req>(h: Handler<Req>) => async (req: Req, next: NextFunction) => {
export const applyHandler = <Req, Res>(h: Handler<Req, Res>) => async (req: Req, res: Res, next: NextFunction) => {
try {
if (h.constructor.name === 'AsyncFunction') {
await h(req, next)
} else h(req, next)
await h(req, res, next)
} else h(req, res, next)
} catch (e) {
next(e)
}
Expand Down Expand Up @@ -47,21 +48,25 @@ export type TemplateEngineOptions<O = any> = Partial<{
_locals: Record<string, any>
}>

export class App<RenderOptions = any, Req extends Request = Request> extends Router<App, Req> {
export class App<RenderOptions = any, Req extends Request = Request, Res extends Response = Response> extends Router<
App,
Req,
Res
> {
middleware: Middleware<Req>[] = []
locals: Record<string, string> = {}
noMatchHandler: Handler
onError: ErrorHandler
settings: AppSettings
engines: Record<string, TemplateFunc<RenderOptions>> = {}
applyExtensions?: (req: Request, next: NextFunction) => void
applyExtensions?: (req: Req, res: Res, next: NextFunction) => void

constructor(
options: Partial<{
noMatchHandler: Handler<Req>
onError: ErrorHandler
settings: AppSettings
applyExtensions: (req: Request, next: NextFunction) => void
applyExtensions: (req: Req, res: Res, next: NextFunction) => void
}> = {}
) {
super()
Expand All @@ -79,7 +84,7 @@ export class App<RenderOptions = any, Req extends Request = Request> extends Rou
return app
}

use(...args: UseMethodParams<Req, any, App>) {
use(...args: UseMethodParams<Req, Res, App>) {
const base = args[0]

const fns: any[] = args.slice(1)
Expand Down Expand Up @@ -128,13 +133,17 @@ export class App<RenderOptions = any, Req extends Request = Request> extends Rou
const { xPoweredBy } = this.settings
if (xPoweredBy) req.headers.set('X-Powered-By', typeof xPoweredBy === 'string' ? xPoweredBy : 'tinyhttp')

const exts = this.applyExtensions || extendMiddleware<RenderOptions>(this as any)
let res = {
headers: new Headers({})
}

const exts = this.applyExtensions || extendMiddleware<RenderOptions, Req, Res>(this as any)

req.originalUrl = req.url || req.originalUrl

const { pathname } = parse(req.originalUrl)

const mw: Middleware[] = [
const mw: Middleware<Req, Res>[] = [
{
handler: exts,
type: 'mw',
Expand All @@ -148,7 +157,7 @@ export class App<RenderOptions = any, Req extends Request = Request> extends Rou
}
]

const handle = (mw: Middleware) => async (req: Req, next: NextFunction) => {
const handle = (mw: Middleware<Req, Res>) => async (req: Req, res: Res, next: NextFunction) => {
const { path = '/', handler, type, regex = rg('/') } = mw

req.url = lead(req.url.substring(path.length)) || '/'
Expand All @@ -157,14 +166,14 @@ export class App<RenderOptions = any, Req extends Request = Request> extends Rou

if (type === 'route') req.params = getURLParams(regex, pathname)

await applyHandler<Req>((handler as unknown) as Handler<Req>)(req, next)
await applyHandler<Req, Res>((handler as unknown) as Handler<Req, Res>)(req, res, next)
}

let idx = 0

next = next || ((err) => (err ? this.onError(err, req) : loop()))
next = next || ((err: any) => (err ? this.onError(err, req) : loop()))

const loop = () => idx < mw.length && handle(mw[idx++])(req, next as NextFunction)
const loop = () => idx < mw.length && handle(mw[idx++])(req, (res as unknown) as Res, next as NextFunction)

loop()
}
Expand Down
8 changes: 5 additions & 3 deletions example/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { App } from '../app.ts'

const app = new App()

app.use('/', (req, next) => {
app.use('/', (req, res, next) => {
console.log(`${req.method} ${req.url}`)

res.headers.set('Test-Header', 'Value')

next()
})

app.get('/:name/', (req) => {
req.respond({ body: `Hello ${req.params.name}!` })
app.get('/:name/', (req, res) => {
res.send(`Hello on ${req.url} from Deno and tinyhttp! 🦕`)
})

app.listen(3000, () => console.log(`Started on :3000`))
24 changes: 22 additions & 2 deletions extend.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { NextFunction } from 'https://esm.sh/@tinyhttp/router'
import { App } from './app.ts'
import { Request } from './request.ts'
import { getRequestHeader, getFreshOrStale } from './extensions/req/headers.ts'
import { send } from './extensions/res/send.ts'
import { Response } from './response.ts'

export const extendMiddleware = <RenderOptions = unknown>(app: App) => (req: Request, next: NextFunction) => {
export const extendMiddleware = <
RenderOptions = unknown,
Req extends Request = Request,
Res extends Response = Response
>(
app: App
) => (req: Req, res: Res, next?: NextFunction) => {
const { settings } = app

// Request extensions
if (settings?.bindAppToReqRes) {
req.app = app
}
next()

req.get = getRequestHeader(req)

if (settings?.freshnessTesting) {
req.fresh = getFreshOrStale(req, res)
req.stale = !req.fresh
}

res.send = send(req, res)

next?.()
}
43 changes: 43 additions & 0 deletions extensions/req/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ServerRequest, Response } from 'https://deno.land/std@0.87.0/http/server.ts'
import parseRange, { Options } from 'https://esm.sh/range-parser'
import fresh from 'https://deno.land/x/fresh/mod.ts'

export const getRequestHeader = (req: ServerRequest) => (header: string): string | string[] | null => {
const lc = header.toLowerCase()

switch (lc) {
case 'referer':
case 'referrer':
return req.headers.get('referrer') || req.headers.get('referer')
default:
return req.headers.get(lc)
}
}
export const getRangeFromHeader = (req: ServerRequest) => (size: number, options?: Options) => {
const range = getRequestHeader(req)('Range') as string

if (!range) return

return parseRange(size, range, options)
}

export const getFreshOrStale = (req: ServerRequest, res: Response) => {
const method = req.method
const status = res.status || 200

// GET or HEAD for weak freshness validation only
if (method !== 'GET' && method !== 'HEAD') return false

// 2xx or 304 as per rfc2616 14.26
if ((status >= 200 && status < 300) || status === 304) {
return fresh(
req.headers,
new Headers({
etag: getRequestHeader(req)('ETag') as string,
'last-modified': res.headers?.get('Last-Modified') as string
})
)
}

return false
}
7 changes: 7 additions & 0 deletions extensions/res/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Request } from '../../request.ts'
import { Response } from '../../response.ts'

export const send = (req: Request, res: Response) => (body: string) => {
req.respond({ ...res, body })
return res
}
3 changes: 3 additions & 0 deletions request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ export interface Request extends ServerRequest {
originalUrl: string
app: App
params: Record<string, any>
get: (header: string) => string | string[] | null
fresh?: boolean
stale?: boolean
}
6 changes: 6 additions & 0 deletions response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Response as ServerResponse } from 'https://deno.land/std@0.87.0/http/server.ts'

export interface Response extends ServerResponse {
headers: Headers
send(body: string): Response
}

0 comments on commit 7bfa794

Please sign in to comment.