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.id} />);
});

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 global SUSPENSE_ROOT 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.