Status: Public Preview - In Development ๐จ Made possible by my Sponsor Program ๐ Follow me @harlan_zw ๐ฆ |
- ๐ค Portable Powered by h3, supporting Serverless, Workers, and Node.js workers.
- ๐ณ Fast Param Routing radix3 named params (
/user/:id
,/user/{id}
and/user/**
) - ๐งฉ Composable Design Utility functions for defining your api, handling requests and serving responses
- โ
Built to Test Testing utility package provided:
@unrouted/test-kit
using supertest - ๐ฑ Pluggable hookable hooks, preset and plugin system.
- ๐ฎ Controller Support Create complex API architectures using controller pattern
- ๐น Fetch Payload Types Automatic type definitions for your routes
- ๐๏ธ Easy Prototyping cors enabled by default, easy debugging with consola and composable utility for sirv
- Add the dependency.
# NPM
npm install unrouted
# or Yarn
yarn add unrouted
# or PNPM
pnpm add unrouted
- Create the Unrouted instance.
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.
- Create your routes using composable functions, within setup (setup optional).
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.
- Tell your server to handle the request using
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)
}
Using listhen and h3.
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!')
})
Using connect.
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!')
})
Using express.
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 matching
Response 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 code
Grouping 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 prefix
Node 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 theServerResponse
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 request
Response 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 response
If 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 const 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 const 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.
- Add the dependency
npm install -D @unrouted/test-kit
- Have Unrouted generate types
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.
- Use the test-kit to write tests
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 instance
setup: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
.
- Type:
string
- Default:
/
All routes will be served from this prefix.
- Type:
string
- Default: ``
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.
- Type:
boolean
- Default:
false
Displays debug logs on the bootstrapping and request life cycles.
- Type:
boolean
- Default:
false
Setting the dev
mode to true allows unrouted to generate types.
- Type:
string
- Default:
process.cwd()
Specify the root where we're running things. This is used for type generation and config loading.
- Type:
string
- Default:
unrouted.config.js
Specify the location of a config file.
- Type:
ResolvedPlugin[]
- Default:
[]
- Type:
ResolvedPlugin[]
- Default:
[]
- Type:
Middleware[]|Handle[]
- Default:
[]
- Type:
UnroutedHooks
- Default:
{}
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