Fastify

Estimated reading time: 2 minutes

@kitajs/fastify-html-plugin adds a reply.html() method to Fastify that handles content type headers, Suspense streaming, and automatic doctype injection.

Setup

XSS Protection Required

You must install and configure @kitajs/ts-html-plugin before using this plugin to avoid XSS vulnerabilities. See the Getting Started guide for complete setup instructions including TypeScript configuration and XSS detection.

npm
yarn
pnpm
bun
npm i @kitajs/fastify-html-plugin

Register the plugin on your Fastify instance:

import fastify from 'fastify'
import fastifyKitaHtml from '@kitajs/fastify-html-plugin'

const app = fastify()
await app.register(fastifyKitaHtml)

Sending HTML

Use reply.html() with any JSX expression. The plugin sets the Content-Type header to text/html; charset=utf-8.

app.get('/', (req, reply) => {
  reply.html(<div>Hello World</div>)
})

For async components, reply.html() accepts promises and awaits them before sending.

app.get('/profile', (req, reply) => {
  reply.html(<UserProfile id={req.params.userId} />)
})

For synchronous responses, the plugin calculates and sets the Content-Length header automatically. For Suspense streaming (below), the response uses chunked transfer encoding without a Content-Length header.

Suspense streaming

When the JSX tree contains Suspense components, the plugin automatically switches from a buffered response to a chunked stream. Use req.id as the Suspense rid to tie the stream to the request.

import { Suspense } from '@kitajs/html/suspense'

app.get('/dashboard', (req, reply) => {
  reply.html(
    <Suspense rid={req.id} fallback={<div>Loading...</div>}>
      <AsyncDashboard />
    </Suspense>
  )
})

No manual stream handling is required. The plugin detects Suspense usage through the SuspenseRoot store and calls resolveHtmlStream internally.

Error handling

The plugin supports ErrorBoundary components for catching async errors. Wrap async content in an ErrorBoundary to handle failures gracefully.

import { ErrorBoundary } from '@kitajs/html/error-boundary'
import { Suspense } from '@kitajs/html/suspense'

app.get('/dashboard', (req, reply) => {
  reply.html(
    <ErrorBoundary catch={(error) => <div>Error: {String(error)}</div>}>
      <Suspense rid={req.id} fallback={<div>Loading...</div>}>
        <AsyncDashboard />
      </Suspense>
    </ErrorBoundary>
  )
})

See the Error Boundaries guide for details on error handling patterns.

Auto-doctype

By default, the plugin prepends <!doctype html> to responses that start with an <html> tag. To disable this globally:

await app.register(fastifyKitaHtml, { autoDoctype: false })

To disable it for a single request, use the exported kAutoDoctype symbol:

import { kAutoDoctype } from '@kitajs/fastify-html-plugin'

app.get('/fragment', (req, reply) => {
  reply[kAutoDoctype] = false
  reply.html(<div>Just a fragment</div>)
})

Compatibility

The plugin works with Fastify 4.x and 5.x.