A service worker router with async middleware and neato type-inference inspired by Koa
npm install 8trackWanted: a better logo
> A service worker router with async middleware and neato type-inference inspired by Koa
#### Installation
```
npm install -S 8track
-- Or yarn
yarn add 8track
##### TypeScript
This library is written in TypeScript, so typings are bundled.
#### Basic usage
`typescript
import { Router, handle } from '8track'
const router = new Router()
router.all(.*).use(async (ctx, next) => {Handling ${ctx.event.request.method} - ${ctx.url.pathname}
console.log()${ctx.event.request.method} - ${ctx.url.pathname}
await next()
console.log()
})
router.get/.handle((ctx) => ctx.html('Hello, world!'))
router.all(.*).handle((ctx) => ctx.end('Not found', { status: 404 }))
addEventListener('fetch', (event) => handle({ event, router }))
`
#### Add CORS headers
`typescript
import { Router } from '8track'
const router = new Router()
router.all(.*).use(async (ctx, next) => {
const allowedOrigins = ['https://www.myorigin.com']
const allowedHeaders = ['Content-type', 'X-My-Custom-Header']
const allowedMethods = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
ctx.response.headers.append('Vary', 'Origin')
ctx.response.headers.append('Access-Control-Allow-Origin', allowedOrigins.join(','))
ctx.response.headers.append('Access-Control-Allow-Headers', allowedHeaders.join(','))
ctx.response.headers.append('Access-Control-Allow-Methods', allowedMethods.join(','))
ctx.response.headers.append('Access-Control-Allow-Credentials', 'true')
if (ctx.req.method === 'OPTIONS') {
return ctx.end('', { status: 204 })
}
await next()
})
`
#### Catch all errors and display error page
`typescript
import { Router, getErrorPageHTML } from '8track'
const router = new Router()
addEventListener('fetch', (e) => {
const res = router.getResponseForEvent(e).catch(
(error) =>
new Response(getErrorPageHTML(e.request, error), {
status: 500,
headers: {
'Content-Type': 'text/html',
},
}),
)
e.respondWith(res as any)
})
`
#### Attach new properties to each request
Each Middleware and route handler receives a new copy of the ctx object, but a special object under the data property is mutable and should be used to share data between handlers
`typescript
interface User {
id: string
name: string
}
// Pretend this is a function that looks up a user by ID
async function getUserById(id: string): Promise
return null as any
}
// The describes the shape of the shared data each middleware will use
interface RouteData {
user?: User
}
// This middleware attaches the user associated to the route to the request
const getUserMiddleware: Middleware
ctx.data.user = await getUserById(ctx.params.userId)
await next()
}
const router = new Router
// For all user requests, attach the user
router.all/users/${'userId'}.use(getUserMiddleware)
router.get/users/${'userId'}.handle((ctx) => {`
if (!ctx.data.user) return ctx.end('Not found', { status: 404 })
ctx.json(JSON.stringify(ctx.data.user))
})
#### Sub-router mounting
`typescript
const apiRouter = new Router()
const usersRouter = new Router()
const userBooksRouter = new Router()
usersRouter.get/.handle((ctx) => ctx.end('users-list'))/${'id'}
usersRouter.get.handle((ctx) => ctx.end(user: ${ctx.params.id}))/
userBooksRouter.get.handle((ctx) => ctx.end('books-list'))/${'id'}
userBooksRouter.get.handle((ctx) => ctx.end(book: ${ctx.params.id}))
usersRouter.all/${'id'}/books.use(userBooksRouter)/api/users
apiRouter.all.use(usersRouter)`
Instantiate a new router
`typescript`
const router = new Router<{ logger: typeof console.log }>()
#### .getResponseForEvent(request: FetchEvent): Promise
Given an event, run the matching middleware chain and return the response returned by the chain.
#### Router handlers and middleware
The primary way to interact with the router is to add routes via method tags:
`typescript/api/users
router.post.handle((ctx) => ctx.json({ id: 123 }))`
In the above example, the post tag returns a RouteMatchResult object.
##### Method Matchers
Each of these methods returns a RouteMatchResult object.
- .all\pattern\pattern\
- .get\pattern\
- .post\pattern\
- .put\pattern\
- .patch\pattern\
- .delete\pattern\
- .head\pattern\
- .options\
When you use a template tag on the router, you create a RouteMatchResult.
`typescript/api/users/${'id'}
router.patch // RouteMatchResult`
The RouteMatchResult object allows you to mount a route handler or a middleware that only runs when the pattern is matched.
`typescript/api/users/${'id'}
router.patch.use(async (ctx, next) => {`
console.log('Before: User ID', ctx.params.id)
await next()
console.log('After: User ID', ctx.params.id)
})
#### .handle((ctx: Context) => any)
Mount a route handler that should return an instance of Response
#### .use((ctx: Context, next: () => Promise
Mount a route middleware that can optionally terminate the chain early and handle the request.
`typescript/api/users/${'id'}
router.patch.use(async (ctx, next) => {
console.log('Before: User ID', ctx.params.id)
if (ctx.params.id === '123') {
return ctx.end(Response.redirect(302))
}
await next()
console.log('After: User ID', ctx.params.id)
})
`
Each route handler and middleware receives an instance of Context.
#### Context Properties
- readonly event: FetchEventreadonly params: Params
- response: Response
- data: Data
- url: URL
-
#### Context Methods
- end(body: string | ReadableStream | Response | null, responseInit: ResponseInit = {})html(body: string | ReadableStream, responseInit: ResponseInit = {})
- json(body: any, responseInit: ResponseInit = {})
-
##### What's up with that weird syntax?
8track uses a JavaScript feature called tagged templates in order to extract parameter names from url patterns. TypeScript is able extract types from tagged template literals:
`typescript
const bar = 123
const baz = new Date()
// Extracted type here is a tuple [number, Date]
footesting: ${bar} - ${baz} cool
// But things get interesting when using literal types
// Extracted type here is a tuple ['bar', 'baz']
footesting: ${'bar'} - ${'baz'} cool`
Since the template literal is able to extract a tuple whose types are the _literal values_ passed in, we can utilize generics to describe the shape of the route parameters:
Serves files from Cloudflare KV.
`typescript
import { Router, kvStatic } from '8track'
const router = new Router()
router.all(.).use(kvStatic({ kv: myKvNamespaceVar, maxAge: 24 60 60 30 }))
`
8track comes with a CLI to upload your worker and sync your kv files. In order to use 8track's kv static file middleware, you must upload your files using this CLI.
Add a deploy script to your package.json:
`javascript`
{
"scripts": {
"deploy": "8track deploy --worker dist/worker.js --kv-files dist/client.js,dist/client.css"
}
}
Note: This does not support globs yet!
You'll need the following environment variables set:
`bash``Your Cloudflare API Token
CF_KEYYour Cloudflare account email
CF_EMAILYour Cloudflare account ID
CF_IDThe ID of the namespace
KV_NAMESPACE_IDThe name of the KV namespace you want to use
KV_NAMESPACEThe variable name your KV namespace is bound to
KV_VAR_NAME