Skip to content
This repository was archived by the owner on Jul 31, 2025. It is now read-only.

Commit 38e43cc

Browse files
author
v1rtl
committed
Write the basic working implementation
1 parent 3d67b85 commit 38e43cc

File tree

8 files changed

+307
-1
lines changed

8 files changed

+307
-1
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"deno.enable": true
3+
}

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,29 @@
11
# tinyhttp-deno
2-
Deno port of tinyhttp, 0-legacy, tiny & fast web framework as a replacement of Express
2+
3+
Deno port of [tinyhttp](https://github.com/talentlessguy/tinyhttp), 0-legacy, tiny & fast web framework as a replacement of Express.
4+
5+
> **WARNING!** This port is very unstable and lacks features. It also doesn't have all of the tinyhttp's original extensions.
6+
7+
## Example
8+
9+
```ts
10+
import { App } from ''
11+
12+
const app = new App()
13+
14+
app.use('/', (req, next) => {
15+
console.log(`${req.method} ${req.url}`)
16+
17+
next()
18+
})
19+
20+
app.get('/:name/', (req) => {
21+
req.respond({ body: `Hello ${req.params.name}!` })
22+
})
23+
24+
app.listen(3000, () => console.log(`Started on :3000`))
25+
```
26+
27+
## Changes
28+
29+
Because Deno doesn't have the same API for HTTP, there's no `res` argument. To send responses use `req.respond` instead.

app.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// deno-lint-ignore-file
2+
import { NextFunction, Router, Handler, Middleware, UseMethodParams } from 'https://esm.sh/@tinyhttp/router'
3+
import { onErrorHandler, ErrorHandler } from './onError.ts'
4+
import 'https://deno.land/std@0.87.0/node/global.ts'
5+
import rg from 'https://esm.sh/regexparam'
6+
import { Request } from './request.ts'
7+
import { getURLParams } from 'https://cdn.esm.sh/v15/@tinyhttp/url@1.1.2/esnext/url.js'
8+
import { extendMiddleware } from './extend.ts'
9+
import { serve, Server } from 'https://deno.land/std@0.87.0/http/server.ts'
10+
import { parse } from './parseUrl.ts'
11+
12+
const lead = (x: string) => (x.charCodeAt(0) === 47 ? x : '/' + x)
13+
14+
export const applyHandler = <Req>(h: Handler<Req>) => async (req: Req, next: NextFunction) => {
15+
try {
16+
if (h.constructor.name === 'AsyncFunction') {
17+
await h(req, next)
18+
} else h(req, next)
19+
} catch (e) {
20+
next(e)
21+
}
22+
}
23+
24+
/**
25+
* tinyhttp App has a few settings for toggling features
26+
*/
27+
export type AppSettings = Partial<
28+
Record<'networkExtensions' | 'freshnessTesting' | 'bindAppToReqRes' | 'enableReqRoute', boolean> &
29+
Record<'subdomainOffset', number> &
30+
Record<'xPoweredBy', string | boolean>
31+
>
32+
/**
33+
* Function that processes the template
34+
*/
35+
export type TemplateFunc<O> = (
36+
path: string,
37+
locals: Record<string, any>,
38+
opts: TemplateEngineOptions<O>,
39+
cb: (err: Error, html: unknown) => void
40+
) => void
41+
42+
export type TemplateEngineOptions<O = any> = Partial<{
43+
cache: boolean
44+
ext: string
45+
renderOptions: Partial<O>
46+
viewsFolder: string
47+
_locals: Record<string, any>
48+
}>
49+
50+
export class App<RenderOptions = any, Req extends Request = Request> extends Router<App, Req> {
51+
middleware: Middleware<Req>[] = []
52+
locals: Record<string, string> = {}
53+
noMatchHandler: Handler
54+
onError: ErrorHandler
55+
settings: AppSettings
56+
engines: Record<string, TemplateFunc<RenderOptions>> = {}
57+
applyExtensions?: (req: Request, next: NextFunction) => void
58+
59+
constructor(
60+
options: Partial<{
61+
noMatchHandler: Handler<Req>
62+
onError: ErrorHandler
63+
settings: AppSettings
64+
applyExtensions: (req: Request, next: NextFunction) => void
65+
}> = {}
66+
) {
67+
super()
68+
this.onError = options?.onError || onErrorHandler
69+
this.noMatchHandler = options?.noMatchHandler || this.onError.bind(null, { code: 404 })
70+
this.settings = options.settings || { xPoweredBy: true }
71+
this.applyExtensions = options?.applyExtensions
72+
}
73+
74+
route(path: string): App {
75+
const app = new App()
76+
77+
this.use(path, app)
78+
79+
return app
80+
}
81+
82+
use(...args: UseMethodParams<Req, any, App>) {
83+
const base = args[0]
84+
85+
const fns: any[] = args.slice(1)
86+
87+
if (base === '/') {
88+
for (const fn of fns) {
89+
if (Array.isArray(fn)) {
90+
super.use(base, fn)
91+
} else {
92+
super.use(base, fns)
93+
}
94+
}
95+
} else if (typeof base === 'function' || base instanceof App) {
96+
super.use('/', [base, ...fns])
97+
} else if (fns.some((fn) => fn instanceof App) && typeof base === 'string') {
98+
super.use(
99+
base,
100+
fns.map((fn: App) => {
101+
if (fn instanceof App) {
102+
fn.mountpath = typeof base === 'string' ? base : '/'
103+
fn.parent = this as any
104+
}
105+
106+
return fn as any
107+
})
108+
)
109+
} else super.use(...args)
110+
111+
return this // chainable
112+
}
113+
find(url: string, method: string) {
114+
return this.middleware.filter((m) => {
115+
if (!m.path) m.path = '/'
116+
m.regex = m.type === 'mw' ? rg(m.path, true) : rg(m.path)
117+
118+
return (m.method ? m.method === method : true) && m.regex.pattern.test(url)
119+
})
120+
}
121+
/**
122+
* Extends Req / Res objects, pushes 404 and 500 handlers, dispatches middleware
123+
* @param req Req object
124+
* @param res Res object
125+
*/
126+
handler(req: Req, next?: NextFunction) {
127+
/* Set X-Powered-By header */
128+
const { xPoweredBy } = this.settings
129+
if (xPoweredBy) req.headers.set('X-Powered-By', typeof xPoweredBy === 'string' ? xPoweredBy : 'tinyhttp')
130+
131+
const exts = this.applyExtensions || extendMiddleware<RenderOptions>(this as any)
132+
133+
req.originalUrl = req.url || req.originalUrl
134+
135+
const { pathname } = parse(req.originalUrl)
136+
137+
const mw: Middleware[] = [
138+
{
139+
handler: exts,
140+
type: 'mw',
141+
path: '/'
142+
},
143+
...this.find(pathname, req.method),
144+
{
145+
handler: this.noMatchHandler,
146+
type: 'mw',
147+
path: '/'
148+
}
149+
]
150+
151+
const handle = (mw: Middleware) => async (req: Req, next: NextFunction) => {
152+
const { path = '/', handler, type, regex = rg('/') } = mw
153+
154+
req.url = lead(req.url.substring(path.length)) || '/'
155+
156+
req.path = parse(req.url).pathname
157+
158+
if (type === 'route') req.params = getURLParams(regex, pathname)
159+
160+
await applyHandler<Req>((handler as unknown) as Handler<Req>)(req, next)
161+
}
162+
163+
let idx = 0
164+
165+
next = next || ((err) => (err ? this.onError(err, req) : loop()))
166+
167+
const loop = () => idx < mw.length && handle(mw[idx++])(req, next as NextFunction)
168+
169+
loop()
170+
}
171+
172+
/**
173+
* Creates HTTP server and dispatches middleware
174+
* @param port server listening port
175+
* @param Server callback after server starts listening
176+
* @param host server listening host
177+
*/
178+
async listen(port: number, cb?: () => void, hostname = '0.0.0.0'): Promise<Server> {
179+
const server = serve({ port, hostname })
180+
181+
cb?.()
182+
183+
for await (const req of server) {
184+
this.handler(req as any)
185+
}
186+
return server
187+
}
188+
}

example/mod.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { App } from '../app.ts'
2+
3+
const app = new App()
4+
5+
app.use('/', (req, next) => {
6+
console.log(`${req.method} ${req.url}`)
7+
8+
next()
9+
})
10+
11+
app.get('/:name/', (req) => {
12+
req.respond({ body: `Hello ${req.params.name}!` })
13+
})
14+
15+
app.listen(3000, () => console.log(`Started on :3000`))

extend.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextFunction } from 'https://esm.sh/@tinyhttp/router'
2+
import { App } from './app.ts'
3+
import { Request } from './request.ts'
4+
5+
export const extendMiddleware = <RenderOptions = unknown>(app: App) => (req: Request, next: NextFunction) => {
6+
const { settings } = app
7+
8+
if (settings?.bindAppToReqRes) {
9+
req.app = app
10+
}
11+
next()
12+
}

onError.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ServerRequest } from 'https://deno.land/std@0.87.0/http/server.ts'
2+
import { NextFunction } from 'https://esm.sh/@tinyhttp/router'
3+
import { ALL as STATUS_CODES } from 'https://deno.land/x/status@0.1.0/codes.ts'
4+
import { status } from 'https://deno.land/x/status@0.1.0/status.ts'
5+
import { Buffer } from 'https://deno.land/std@0.77.0/node/buffer.ts'
6+
7+
export type ServerError = Partial<{
8+
code: number
9+
status: number
10+
message: string
11+
}>
12+
13+
export type ErrorHandler = (err: ServerError, req: ServerRequest, next?: NextFunction) => void
14+
15+
export const onErrorHandler: ErrorHandler = async (err: ServerError, req: ServerRequest) => {
16+
let code = 500
17+
18+
if (err.code && err.code in STATUS_CODES) code = err.code
19+
else if (err.status) code = err.status
20+
21+
if (typeof err === 'string' || Buffer.isBuffer(err)) {
22+
await req.respond({
23+
body: err,
24+
status: 500
25+
})
26+
} else if (code in STATUS_CODES) {
27+
await req.respond({
28+
body: status.message[code],
29+
status: 500
30+
})
31+
} else
32+
req.respond({
33+
status: 500,
34+
body: err.message
35+
})
36+
}

parseUrl.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as qs from 'https://deno.land/std@0.88.0/node/querystring.ts'
2+
3+
export const pathname = (u: string) => {
4+
const end = u.indexOf('?')
5+
6+
return u.slice(0, end === -1 ? u.length : end)
7+
}
8+
9+
export const parse = (url: string) => {
10+
const path = pathname(url)
11+
12+
const query = qs.parse(url.slice(url.indexOf('?')))
13+
14+
return { pathname: path, query }
15+
}

request.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// deno-lint-ignore-file
2+
import { ServerRequest } from 'https://deno.land/std@0.87.0/http/server.ts'
3+
import { App } from './app.ts'
4+
5+
export interface Request extends ServerRequest {
6+
path: string
7+
originalUrl: string
8+
app: App
9+
params: Record<string, any>
10+
}

0 commit comments

Comments
 (0)