write your request handlers once, run anywhere
currently supports:
- Node (vanilla HTTP)
- Express
- Fastify
- Fetch (Cloudflare Workers, Deno, SvelteKit, Astro, Remix, etc.)
- h3 (Nuxt)
- Koa
- AWS Lambda
npm install uttp
First, define your universal request handler:
// handler.ts
import { defineHandler } from 'uttp'
export const handler = defineHandler(() => {
// return an object that will be used by each adapter
return {
// called on each request
handleRequest() {
// return a response object
// that will be sent by the server framework
return {
status: 200,
body: 'Hello world!',
headers: { 'Content-Type': 'text/html' },
}
},
adapterOptions: {},
}
})
For all server frameworks uttp supports this will show Hello world!
as HTML.
Then you can use adapters to get middleware/plugins/handlers for the server frameworks.
For Node:
// adapters/node.ts
import { getNodeAdapter } from 'uttp/adapters/node'
import { handler } from '../handler'
export const nodeHandler = getNodeAdapter(handler)
Users would use it like this:
import { nodeHandler } from 'my-lib/adapters/node'
const server = createServer(await nodeHandler())
server.listen(3000)
This process is the same for other server frameworks.
For Fastify:
// adapters/fastify.ts
import { getFastifyAdapter } from 'uttp/adapters/fastify'
import { handler } from '../handler'
export const getFastifyPlugin = getFastifyAdapter(handler)
Users would use it like this:
import { getFastifyPlugin } from 'my-lib/adapters/fastify'
const server = fastify()
server.register(await getFastifyPlugin())
server.listen(3000)
Note these are placed in different entry points / files because uttp/adapters/*
imports directly from the server frameworks. You cannot export multiple handlers from the same entry point because users would be forced to install server frameworks that they are not using.
A universal request object is passed to handleRequest
containing some common properties coerced from the individual frameworks:
import { defineHandler } from 'uttp'
export const handler = defineHandler(() => {
return {
handleRequest(req) {
if (req.method !== 'GET') {
return { status: 400, body: 'method must be get' }
}
return {
status: 200,
body: 'Hello world!',
headers: { 'Content-Type': 'text/html' },
}
},
adapterOptions: {},
}
})
Request handlers are passed a set of universal functions that vary in implementation across frameworks but retain the same signature:
import { defineHandler } from 'uttp'
export const handler = defineHandler((helpers) => {
return {
async handleRequest(req) {
// each adapter will pass helpers
// that conform to function signatures
const body = await helpers.parseBodyAsString(req.rawRequest)
if (!body) {
return { status: 400, body: 'must pass body' }
}
const json = JSON.parse(body)
json.
return {
status: 200,
body: 'Hello world!',
headers: { 'Content-Type': 'text/html' },
}
},
adapterOptions: {},
}
})
If you need a helper that is not currently available, please create an issue.
Your request handler can take in options from users of your handler:
import { defineHandler } from 'uttp'
interface HandlerOptions {
parse(text: string): any | Promise<any>
maxBodySize?: number
}
export const handler = defineHandler(
// specify options type here
// can specify as many arguments as you want after `helpers`
// which the user will need to pass
(helpers, options: HandlerOptions) => {
return {
async handleRequest(req) {
const body = await helpers.parseBodyAsString(req.rawRequest)
if (!body) return { status: 400, body: 'must have body' }
const parsedBody = await options.parse(body)
// ...
return { status: 200 }
},
adapterOptions: {
maxBodySize: options.maxBodySize,
},
}
},
)
Users will pass options like this:
import { nodeHandler } from 'my-lib/adapters/node'
const server = createServer(await nodeHandler({ parse: JSON.parse }))
server.listen(3000)
You must return an adapterOptions
object. These options may be derived from user options. Here is an example with a description of what each option does:
import { defineHandler } from 'uttp'
export const handler = defineHandler(() => {
return {
handleRequest() {
return { status: 200, body: 'Hello world!' }
},
adapterOptions: {
// limit body size
maxBodySize: 1000,
},
}
})
See starter templates for how to setup a package that uses uttp.
uttp comes with some utils to help you build and test your handler.
Runners are an easy way to get a server up for a framework by providing your handler. Only some frameworks are supported.
import {
runNode,
// runExpress,
// runFastify,
// runH3,
// runKoa,
} from 'uttp/utils/runners'
// your universal handler
import { handler } from './handler.js'
runNode(
handler,
// handler options as an array
[{ token: 'secret' }],
// server-related options
{ port: 3000 },
)