Unrouted is a minimal, composable router built for speed, portability and DX
/user/:id
, /user/{id}
and /user/**
)@unrouted/test-kit
using supertest
# NPM
npm install unrouted
# or Yarn
yarn add unrouted
# or PNPM
pnpm add unrouted
import { createUnrouted } from 'unrouted'
// ...
async function createApi() {
const { setup, handle } = await createUnrouted({
// options
})
}
Creating unrouted will return the Unrouted Context. To get your API setup, you need to make use of two functions: setup and handle.
import { createUnrouted, get } from 'unrouted'
// ...
async function createApi() {
const { setup, app } = await createUnrouted({
// options
})
await setup(() => {
get('/', 'hello world')
})
}
Note: The setup
function ensures the unrouted context is used by the utility functions and lets us perform
hooks on the final routes provided by your API, such as generating types.
handle
.import { createUnrouted, get } from 'unrouted'
// ...
async function createApi() {
const { setup, app } = await createUnrouted({
// options
})
await setup(() => {
get('/', 'hello world')
})
// app could be h3, koa, connect, express servers
app.use(app.nodeHandler)
}
import { createUnrouted, get } from 'unrouted'
import { createApp } from 'h3'
import { listen } from 'listhen'
async function createApi() {
// ctx is the unrouted context
const { setup, app } = await createUnrouted({
// options
})
await setup(() => {
get('/', 'hello world')
})
return app
}
async function boot() {
const app = createApp()
app.use(await createApi())
listen(app)
}
boot().then(() => {
console.log('Ready!')
})
import { createUnrouted, get } from 'unrouted'
import createConnectApp from 'connect'
async function createApi() {
// ctx is the unrouted context
const { setup, app } = await createUnrouted({
// options
})
await setup(() => {
get('/', 'hello world')
})
return app.nodeHandler
}
async function boot() {
const app = createConnectApp()
app.use(await createApi())
}
boot().then(() => {
console.log('Ready!')
})
import { createUnrouted, get } from 'unrouted'
import createExpressApp from 'express'
async function createApi() {
// ctx is the unrouted context
const { setup, app } = await createUnrouted({
// options
})
await setup(() => {
get('/hello-world', 'api is working')
post('/contact', () => {
const { email } = useBody<{ email: string }>()
return {
success: true,
email,
}
})
})
return app.nodeHandler
}
async function boot() {
const app = createExpressApp()
app.use(await createApi())
}
boot().then(() => {
console.log('Ready!')
})
Verbs
get(path: string, res)
- GET routepost(path: string, res)
- POST routeput(path: string, res)
- PUT routedel(path: string, res)
- DELETE routehead(path: string, res)
- HEAD routeoptions(path: string, res)
- OPTIONS routeany(path: string, res)
- Matches any HTTP methodmatch(method: string, path: string, res)
- Matches a specific HTTP method, useful for dynamic method matchingResponse Utils
permanentRedirect(path: string, toPath: string)
- Performs a permanent redirectredirect(path: string, toPath: string, statusCode: number = 302)
- Performs a temproary redirect by default, you can change the status codeGrouping utils
group(prefix: string, () => void)
- Allows you to group composables under a specific prefixmiddleware(prefix: string, () => void)
- Allows you to group composables under a specific prefixprefix(prefix: string, () => void)
- Allows you to group composables under a specific prefixNode only
serve(path: string, dirname: string, sirvOptions: Options = {})
- Serve static content using sirv
res
is a function similar to standard middleware.
get('/', (request: IncomingMessage, res: ServerResponse) => {
return 'hello world'
})
Since Unrouted is composable, you may not need to use these arguments.
get('/', 'hello world')
You can return the following as a primitive or as an async / sync function which returns a primitive:
string|boolean
- Will be assumed an HTML response and set the content-type to text/htmlnumber
- Will be assumed a status codeobject
- Will be assumed a JSON response and set the content-type to application/jsonvoid
- You can modify the ServerResponse
directly and return nothing// text/html -> 'api is working' - 200
get('/hello-world', 'api is working')
// application/json -> { success: true, time: 1245456789 } - 200
post('/time', () => {
return {
success: true,
time: new Date().toTimeString(),
}
})
get('/secret-zone', async (req, res) => {
const authenticated = await authenticate()
// Example where we use the response directly
if (!authenticated) {
res.statusCode = 401
res.end()
// we can return void here
return
}
// using the request directly
if (!authenticated && req.headers['x-secret-token'] !== 'secret') {
// can simply return an integer as the status code response
return 401
}
return {
success: true,
message: 'Welcome to the secret zone!',
}
})
Use of the setup
function is optional.
By defining all of your routes in a predictable way unrouted is able
to provide runtime enhancements through the hooks' system, such as generating types.
For example plugins can make use of the defined routes as:
const { hooks } = useUnrouted()
hooks.hook('setup:after', (ctx) => {
// ctx.routes contains all of the routes defined in the setup function
})
The two main functions you'll use are useBody
and useParams
, both are provided as composables with generics.
Body and Params example
interface User {
name: string
age: number
}
post('/user/:name', () => {
const { name } = useParams<{ name: string }>()
const { age } = useBody<User>()
// ...
return {
success: true,
user: {
name,
age
}
}
})
const { name } = useBody<{ name: string }>()
// ts works, name is a string
console.log(name.toUpperCase())
Note: Unrouted does not come with validation.
Most functions provided by h3 are exposed on unrouted
as composable utilities.
See the h3 docs for more details.
Request Utils
useRequest()
- Returns the request objectuseRawBody(encoding?: string)
- Reads the raw body of the requestuseQuery<T>()
- Reads the query string of the request, has generics supportuseMethod(defaultMethod?: string)
- Reads the HTTP method of the requestisMethod(method: string)
- Checks if the request method is the same as the provided methodassertMethod(method: string)
- Asserts that the request method is the same as the provided methoduseCookies()
- Reads the cookies of the requestuseCookies(name: string)
- Reads a specific cookie of the requestResponse Utils
useResponse()
- Returns the response objectsetCookie(name: string, value: string, serializeOptions?: any)
- Sets cookie on the responsesendRedirect(path: string, statusCode?: number)
- Performs a redirectsetStatusCode(statusCode: number)
- Sets the status code of the responsesendError(error: Error | H3Error)
- Sends an error responseappendHeader(name: string, value: string)
- Appends a header to the responseIf you'd like to create your own composable utility functions,
you can use the low-level registerRoute
or use the existing composable functions.
Examples
Using registerRoute
we create a new composable function to deny certain paths.
export function deny(route: string) {
registerRoute('*', route, () => {
setStatusCode(400)
return {
success: false,
error: 'you\'re not allowed here'
}
})
}
// ...
deny('/private-zone/**')
We can build on top of existing composable functions to create more complex utilities.
export function resource(route: string, factory) {
get(route, factory.getAll)
group(`${route}/:id`, () => {
get('/', factory.getResource)
post('/', factory.saveResource)
del('/', factory.deleteResource)
})
}
// ...
resource('/posts', factory)
Unrouted comes with package called @unrouted/test-kit
which provides a simple way to write tests that make use of
generated types.
npm install -D @unrouted/test-kit
import { createUnrouted } from 'unrouted'
await createUnrouted({
// dev should be dynamic, must be on to generate types
dev: true,
generateTypes: true,
// Optional: if you want to change the output directory of the routes
root: join(__dirname, '__routes__')
})
Now when your code next runs the setup function, the route definitions will be generated.
Here we bootstrap Unrouted on our server (such as connect) and create a request
instance which we'll use to test.
import { test } from '@unrouted/test-kit'
// this should point to your routes
import { RequestPathSchema } from '../../routes.d.ts'
// createApi is a function which builds the api and returns the handle function
const api = await createApi({ debug: true })
// tell our server to use the api
app.use(api)
// create a test request instance
const request = testKit<RequestPathSchema>(app)
Now you can start testing. See supertest documentation for further testing instructions.
// /hello-world is autocompleted
request.get('/hello-world')
createUnrouted
- Create the unrouted instancedefineConfig
- Define unrouted configdefineUnroutedPlugin
- Define an unrouted plugindefineUnroutedPreset
- Define an unrouted presetuseUnrouted
- Use the global unrouted instancesetup:before: (ctx: UnroutedContext) => HookResult;
Called before the setup()
function starts. No routes are available yet.
setup:after: (ctx: UnroutedContext) => HookResult
Called after the setup()
function is finished. At this point, routes are normalised and registered.
setup:routes: (routes: Route[]) => HookResult
Called when hooks are normalised, can be used to transform the hooks before they are registered to the router.
request:payload: (ctx: PayloadCtx) => HookResult
When the payload is resolved from your routes.
request:lookup:before
: (requestPath: string) => HookResult;Before the radix3 router is used to look up the route path.
request:error:404
: (requestPath: string, req: IncomingMessage) => HookResult;By default, unrouted, does not handle 404s; this lets you handle it.
Example
import { useUnrouted } from 'unrouted'
const { hooks } = useUnrouted()
hooks.hook('setup:before', () => {
console.log('before setup')
})
You can provide configuration to the createUnrouted
function directly, provide a unrouted.config.ts
file or link
a configuration file using configFile
.
string
/
All routes will be served from this prefix.
string
Setting a name for the unrouted context will allow you to generate contextual types and have custom scoped debugging logs.
If you only plan to have a single instance of Unrouted, this will likely not be needed.
boolean
false
Displays debug logs on the bootstrapping and request life cycles.
boolean
false
Setting the dev
mode to true allows unrouted to generate types.
string
process.cwd()
Specify the root where we're running things. This is used for type generation and config loading.
string
unrouted.config.js
Specify the location of a config file.
ResolvedPlugin[]
[]
ResolvedPlugin[]
[]
Middleware[]|Handle[]
[]
UnroutedHooks
{}
export interface UnroutedContext {
/**
* Runtime configuration for the current prefix path.
*/
prefix: string
/**
* Resolved configuration.
*/
config: ResolvedConfig
/**
* Function used to handle a request for the Unrouted instance.
* This should be passed to a server such as h3, connect, express, koa, etc.
*/
handle: HandleFn
/**
* A flat copy of the normalised routes being used.
*/
routes: Route[]
/**
* The routes grouped by method, this is internally used by the handle function for quicker lookups.
*/
methodStack: Record<HttpMethod, (RadixRouter<Route> | null)>
/**
* The logger instance. Will be Consola if available, otherwise console.
*/
logger: Consola | Console
/**
* The hookable instance, allows hooking into core functionality.
*/
hooks: UnroutedHookable
/**
* Composable setup function for declaring routes.
* @param fn
*/
setup: (fn: () => void) => Promise<void>
}
MIT License 2022 Harlan Wilton