Standard Server provides a unified interface for client-server communication over HTTP and message-based transports.
import type { StandardLazyRequest, StandardResponse, } from '@standardserver/core'
export async function handle(request: StandardLazyRequest): Promise<StandardResponse> {
const body = await request.resolveBody()
return {
status: 200,
headers: { 'content-type': 'application/json' },
async* body() { // <- SSE response
yield 'Hello, World!'
},
}
}Standard Server abstracts away the complexities of handling different communication protocols, allowing developers to focus on building their applications without worrying about the underlying transport mechanisms. It supports both HTTP and message-based transports, making it versatile for various use cases.
| Package | Purpose | Main entry points |
|---|---|---|
@standardserver/core |
Shared types, validators, and utilities | StandardRequest, StandardLazyRequest, StandardResponse, StandardLazyResponse |
@standardserver/fetch |
Fetch API adapter | toStandardLazyRequest, toFetchResponse, toStandardLazyResponse, toFetchBody, toFetchHeaders |
@standardserver/node |
Node.js HTTP/HTTP2 adapter | toStandardLazyRequest, sendStandardResponse |
@standardserver/peer |
Message-based adapter | ClientPeer, ServerPeer, encodePeerMessage, decodePeerMessage |
Standard Server defines four core types: StandardRequest, StandardLazyRequest, StandardResponse, and StandardLazyResponse. Together, they provide a consistent shape for transport-agnostic communication.
| Field | Type | Description |
|---|---|---|
method |
string |
HTTP method (e.g., GET, POST) |
url |
string |
request URL not include origin and start with / |
headers |
Record<string, string | string[] | undefined> |
request headers as a lowercase-key-value pair |
body |
StandardBody |
parsed body |
resolveBody(hint?) |
(hint?: StandardBodyHint) => Promise<StandardBody> |
Lazily resolves the request body |
status |
number |
HTTP status code (e.g., 200, 404) |
signal |
undefined | AbortSignal |
signal related to request/response lifecycle |
Currently, StandardBody and StandardBodyHint can be one of the following types:
| Type | Hint | Description | Content-Type |
|---|---|---|---|
unknown |
json |
JSON-compatible value | application/json |
FormData |
form-data |
Multipart form submissions | multipart/form-data |
URLSearchParams |
url-encoded |
URL-encoded forms | application/x-www-form-urlencoded |
AsyncIteratorObject |
event-stream |
Server-Sent Events (SSE) | text/event-stream |
ReadableStream<Uint8Array> |
octet-stream |
Binary streaming | any |
Blob or File |
file |
Fixed-size binary payload | any |
undefined |
none |
Empty body |
resolveBody(hint?) determines how to parse the body using the following priority:
- If the
standard-serverheader is present, use it as theStandardBodyHint. - Otherwise, if
hint?is provided, use it as theStandardBodyHint. - Otherwise, if
content-typeis one of the common types, parse accordingly. - Otherwise, if
content-lengthexists, treat the body asfile; if not, treat it asoctet-stream.
For efficient communication, set the standard-server header to explicitly hint the body type, especially for file or binary streaming. For example, if you upload a file with a common content-type such as application/json but omit the standard-server header, the server may interpret it as JSON and parse it unexpectedly.
const response = await fetch('/upload', {
method: 'POST',
headers: {
'content-type': 'application/json',
'standard-server': 'file', // <- hint the body type to avoid misinterpretation
},
body: new Blob(['{"message": "Hello, world!"}'], { type: 'application/json' }),
})Standard Server treats primitive values, objects, and arrays as JSON.
import { StandardRequest } from '@standardserver/core'
const request: StandardRequest = {
method: 'POST',
url: '/submit',
headers: {},
body: { name: 'John Doe', email: 'john.doe@example.com' },
}Standard Server treats FormData and URLSearchParams as form submissions.
import { StandardRequest } from '@standardserver/core'
const requestWithURLSearchParams: StandardRequest = {
method: 'POST',
url: '/submit',
headers: {},
body: new URLSearchParams({ name: 'John Doe', email: 'john.doe@example.com' }),
}
const formData = new FormData()
formData.append('name', 'John Doe')
formData.append('file', new Blob(['Hello, World!'], { type: 'text/plain' }), 'hello.txt')
const requestWithFormData: StandardRequest = {
method: 'POST',
url: '/submit',
headers: {},
body: formData,
}Tip
HTML forms submit data as application/x-www-form-urlencoded or multipart/form-data, so this is especially helpful here.
Standard Server treats File and Blob as fixed-size binary payloads.
Note
Since File extends Blob, resolveBody always returns a File when representing either File or Blob bodies.
import { StandardResponse } from '@standardserver/core'
const response: StandardResponse = {
status: 200,
headers: {
'content-disposition': [], // <- remove auto-set header
},
body: new File(['Hello, World!'], 'hello.txt', { type: 'text/plain' }),
}When sending a file or blob body, Standard Server automatically sets the content-length, content-type, content-disposition, and standard-server headers based on the provided body. You can override content-disposition by explicitly providing a header value, or remove it entirely by assigning an empty array.
Standard Server uses AsyncIteratorObject to represent an event stream body, and you can use withEventIteratorEventMeta to attach additional SSE event metadata to each emitted event.
import { StandardResponse } from '@standardserver/core'
import { EventIteratorErrorEvent, withEventIteratorEventMeta } from '@standardserver/core/event-stream'
const response: StandardResponse = {
status: 200,
headers: {},
async* body() {
yield withEventIteratorEventMeta(
{ message: 'Hello, World!' },
{ id: '1', retry: 3000, comments: ['hidden'] },
)
throw EventIteratorErrorEvent({ message: 'Something went wrong' })
return { message: 'This is the end of the stream' }
},
}Events are interpreted as follows: yield emits a message, throw emits an error, and return emits a close event. Note that close does not cause EventSource to close the connection because it is not part of the SSE specification. However, when using Standard Server for client-side streaming, close is treated as the end of the stream, so the connection is closed and no reconnection is attempted.
Standard Server uses ReadableStream to represent a binary streaming body.
import { StandardResponse } from '@standardserver/core'
const response: StandardResponse = {
status: 200,
headers: {
'content-type': 'application/octet-stream',
},
body: new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder()
controller.enqueue(encoder.encode('Hello, World!'))
controller.close()
},
}),
}When sending a binary streaming body, Standard Server automatically sets the content-type and standard-server headers. You can override content-type by providing an explicit header value, or remove it entirely by assigning an empty array.
Use @standardserver/fetch to integrate with the Fetch API. For detailed implementation, see the Fetch API adapter documentation.
import { toFetchBody, toFetchHeaders, toFetchResponse, toStandardLazyRequest, toStandardLazyResponse } from '@standardserver/fetch'
// server-side
export async function handleFetchRequest(request: Request): Promise<Response> {
const standardLazyRequest = toStandardLazyRequest(request)
const standardResponse = await handle(standardLazyRequest)
return toFetchResponse(standardResponse, {/** options */})
}
// client-side
export async function main() {
const standardRequest = {
method: 'GET',
url: '/api/data',
headers: {},
body: { message: 'Hello, World!' },
}
const [body, standardHeaders] = toFetchBody(standardRequest.body, standardRequest.headers, {/** options */})
const response = await fetch(standardRequest.url, {
method: standardRequest.method,
headers: toFetchHeaders(standardHeaders),
body,
})
const standardLazyResponse = toStandardLazyResponse(response)
}Use @standardserver/node to integrate with Node.js HTTP and HTTP/2. For implementation details, see the Node.js adapter documentation.
import type { IncomingMessage, ServerResponse } from 'node:http'
import { createServer } from 'node:http'
const server = createServer(async (req, res) => {
const standardLazyRequest = toStandardLazyRequest(req)
const standardResponse = await handle(standardLazyRequest)
await sendStandardResponse(res, standardResponse, {/** options */})
})Tip
When sending requests or responses, you can pass additional options such as event-stream keep-alive.
Unlike HTTP adapters, message-based adapters are built from the ground up to enable client-server communication through string or binary messages. They are ideal for WebSocket, MessagePort, or any custom transport implementations. For detailed implementation, see the peer adapter documentation.
import {
ClientPeer,
decodePeerMessage,
encodePeerMessage,
isClientPeerSendMessage,
isServerPeerSendMessage,
ServerPeer
} from '@standardserver/peer'
const { port1, port2 } = new MessageChannel()
const clientPeer = new ClientPeer(async (message) => {
port1.postMessage(await encodePeerMessage(message, { /** options */ }))
})
const serverPeer = new ServerPeer(async (message) => {
port2.postMessage(await encodePeerMessage(message, { /** options */ }))
})
port1.addEventListener('message', async (event) => {
const decoded = decodePeerMessage(event.data, { /** options */ })
if (decoded.matched && isServerPeerSendMessage(decoded.message)) {
await clientPeer.message(decoded.message)
}
})
port2.addEventListener('message', async (event) => {
const decoded = decodePeerMessage(event.data, { /** options */ })
if (decoded.matched && isClientPeerSendMessage(decoded.message)) {
await serverPeer.message(decoded.message, async (standardLazyRequest) => {
const standardResponse = await handle(standardLazyRequest)
return standardResponse
})
}
})
port1.start()
port2.start()
const standardLazyResponse = await clientPeer.request({
method: 'GET',
url: '/ping',
headers: {},
})Tip
When encoding or decoding peer messages, you can pass additional options, such as prefix, to prevent collisions when the same peer is used for multiple purposes.