fastify-arktype
v0.1.0
Published
Fastify type provider, validator/serializer compilers, and @fastify/swagger integration for ArkType
Downloads
182
Maintainers
Readme
fastify-arktype
A Fastify type provider for ArkType. Write route schemas once in ArkType and get:
- Full TypeScript inference on
request.body,request.query,request.params,request.headers, and typedreply.send()— no generics. - Runtime validation via ArkType, with Fastify-standard 400 error payloads.
- OpenAPI generation for
@fastify/swagger, powered by ArkType's nativetoJsonSchema().
The API mirrors fastify-type-provider-zod, so migrating between them is mindless. Note this is a type provider, not a plugin — there's nothing to register(); you set two compilers and a type parameter (see below).
Install
npm install fastify-arktype arktype fastifyPeer dependencies: fastify@^5, arktype@^2, and optionally @fastify/swagger for OpenAPI generation. Node >= 20.
Setup
import Fastify from "fastify";
import { type } from "arktype";
import {
validatorCompiler,
serializerCompiler,
type ArkTypeTypeProvider,
} from "fastify-arktype";
const app = Fastify()
.setValidatorCompiler(validatorCompiler)
.setSerializerCompiler(serializerCompiler)
.withTypeProvider<ArkTypeTypeProvider>();
// Schemas are plain ArkType types — define them anywhere, reuse them across routes.
// Constraints like number>=0 are validated at runtime and flow into the OpenAPI
// doc (minimum: 0), even where TypeScript can only express `number`.
const NewUser = type({ name: "string", age: "number>=0" });
const UserCreated = type({ id: "string.uuid" });
app.post(
"/users",
{
schema: {
body: NewUser,
response: { 201: UserCreated },
},
},
async (req, reply) => {
req.body.name; // string — inferred, no generics
return reply.code(201).send({ id: crypto.randomUUID() });
},
);A runnable app lives in examples/basic.ts.
Query/params coercion with morphs
Fastify passes querystring, params, and headers as string records. Use ArkType's parsing keywords (morphs) to coerce them — the handler sees the schema's output type:
app.get(
"/items",
{ schema: { querystring: type({ page: "string.numeric.parse" }) } },
async (req) => {
req.query.page; // number
return { page: req.query.page };
},
);Morphs also run on responses before serialization. reply.send() accepts the schema's input type and the morph output goes over the wire:
const responseSchema = type({
createdAt: ["Date", "=>", (d) => d.toISOString()],
});
// handler returns { createdAt: new Date(...) }; the client receives an ISO stringOpenAPI / swagger
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUI from "@fastify/swagger-ui";
import { jsonSchemaTransform } from "fastify-arktype";
await app.register(fastifySwagger, {
openapi: {
openapi: "3.1.1",
info: { title: "my api", version: "1.0.0" },
},
transform: jsonSchemaTransform,
});
await app.register(fastifySwaggerUI, { routePrefix: "/documentation" });Schemas are converted with ArkType's native toJsonSchema(). Constructs JSON Schema can't represent (morphs, protos like Date, predicates) degrade to their closest representable base schema — a string.numeric.parse querystring documents as a pattern-constrained string — instead of throwing.
To skip routes or customize the conversion:
import { createJsonSchemaTransform } from "fastify-arktype";
transform: createJsonSchemaTransform({
skipList: ["/healthcheck"],
// forwarded to Type.toJsonSchema(); see ArkType docs
toJsonSchemaOptions: { fallback: (ctx) => ctx.base ?? {} },
});Routes can also set schema: { hide: true } to be excluded from the document. OpenAPI 3.1 is recommended since ArkType emits JSON Schema draft 2020-12.
Error handling
Validation failures return Fastify's standard 400 payload (code: "FST_ERR_VALIDATION"), with per-issue details on error.validation. Response schema mismatches throw a 500 ResponseSerializationError. Customize both with the exported guards:
import {
hasArkTypeFastifySchemaValidationErrors,
isResponseSerializationError,
} from "fastify-arktype";
app.setErrorHandler((error, req, reply) => {
if (hasArkTypeFastifySchemaValidationErrors(error)) {
// error.validation: [{ keyword, instancePath, message, params: { expected, actual } }]
return reply
.code(400)
.send({ error: "Bad Request", issues: error.validation });
}
if (isResponseSerializationError(error)) {
// error.method, error.url, error.cause (the original ArkErrors)
return reply.code(500).send({ error: "Internal Server Error" });
}
return reply.send(error);
});Mixing with non-ArkType routes
Setting the compilers app-wide means every schema-bearing route goes through them. A route using plain JSON Schema should supply its own per-route compilers:
app.post(
"/legacy",
{
schema: { body: legacyJsonSchema },
validatorCompiler: myAjvCompiler, // route-level override
serializerCompiler: myFastJsonCompiler,
},
handler,
);A non-ArkType response schema without an override fails fast at startup with InvalidSchemaError. The swagger transform, by contrast, passes non-ArkType schemas through untouched, so mixed documents work.
Migrating from fastify-type-provider-zod
Export names match; swap the package and the schemas.
| fastify-type-provider-zod | fastify-arktype |
| ------------------------------------------------------------- | ------------------------------------------------------------ |
| ZodTypeProvider | ArkTypeTypeProvider |
| validatorCompiler | validatorCompiler |
| serializerCompiler / createSerializerCompiler | serializerCompiler / createSerializerCompiler |
| jsonSchemaTransform / createJsonSchemaTransform | jsonSchemaTransform / createJsonSchemaTransform |
| jsonSchemaTransformObject | — (ArkType has no schema registry; schemas are inlined) |
| hasZodFastifySchemaValidationErrors | hasArkTypeFastifySchemaValidationErrors |
| isResponseSerializationError / ResponseSerializationError | same |
| InvalidSchemaError | same |
| FastifyPluginAsyncZod / FastifyPluginCallbackZod | FastifyPluginAsyncArkType / FastifyPluginCallbackArkType |
One semantic difference: Zod v4 codecs are bidirectional, so the Zod provider types reply.send() with the schema's output type and encodes back to the wire format. ArkType morphs are one-way (input → output), so here reply.send() takes the input type and the morph produces the wire format.
FAQ
Why not a generic Standard Schema type provider? Standard Schema standardizes validation and inference, but not JSON Schema export. Swagger/OpenAPI generation — half the value of this package — therefore requires library-specific integration, which is why this package is ArkType-specific.
Does it use fast-json-stringify? No. Responses are validated by ArkType, then serialized with JSON.stringify, same as the Zod provider.
License
MIT
