Better typescript support and runtime safety of fastify handlers
70
stars
237
commits
TypeScript
primary language
Nov 4, 2025
updated
This package adds strong TypeScript support to Fastify request handlers and enforces handlers to have typed schema which is used to validate request params and replies. From this schema it does two things:
request.bodyrequest.headersrequest.querystringrequest.paramsroute.path.params are also inferred and mapped to request.params, it is also not possible to make a typo in schema paramsreply is always based on status, developer won't be able to use plain reply.send() but
forced to explicitly set status first, based on which response type will be inferred@tjs annotations can be used to fine-tune output)
typejescript-json-schema: all known limitations of lib are inherited:
{ [k: string]: string } instead or hint with @tjsnpm i @coobaha/typed-fastify
pnpm i @coobaha/typed-fastify
yarn add @coobaha/typed-fastify
Example of service we want to build
GET / => Hello ($querystring.name || world)
Simple implementation without schema generation will be following
import addSchema, { Schema } from '@coobaha/typed-fastify';
import fastify from 'fastify';
export interface ExampleSchema extends Schema {
paths: {
'GET /': {
request: {
querystring: {
name?: string;
};
};
response: {
200: {
content: string;
};
};
};
};
}
const exampleService: Service<ExampleSchema> = {
'GET /': (req, reply) => {
// typescript will infer correct types for us
const name = req.query.name ?? 'World';
// Calling send directly is not allowed
// reply.send(`Hello ${name}`)
// Calling send with wrong payload will result in an error
// reply.status(200).send(new Date())
return reply.status(200).send(`Hello ${name}`);
},
};
const app = fastify();
addSchema(app, {
// it is strongly recommended to generate json schema to guaruntee runtime validity
jsonSchema: {},
service: exampleService,
});
// Start listening.
app.listen(3000, (err: any) => {
if (err) {
app.log.error(err);
process.exit(1);
}
});
Complex examples can be found typescript tests and in integration.test.ts.
You can generate json schema from your TS types by using typed-fastify-schema or tfs bins
npx tfs gen
tfs gen [files]
Generates json schemas next to corresponding ts files
Positionals:
files glob pattern of files [string] [required]
Options:
--help Show help [boolean]
--version Show version number [boolean]
# it will generate example_schema.gen.json next to file
npx tfs gen example_schema.ts
When schema is generated - just pass it to plugin to have runtime validations 🎉
import jsonSchema from './example_schema.gen.json';
// ...
addSchema(app, {
jsonSchema,
service,
});
Handlers in one object Type inference will work nicely in this case, you just make TS happy and things are working 🥳
Handlers in a different file or separate functions - you will need to hint TS with exact type of handler.
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
const myHandler: RequestHandler<MySchema, 'GET /hello'>['AsRoute'] = (req, reply) => {};
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
const myHandlers: RequestHandler<MySchema, 'GET /hello' | `GET /hello2`>['AsRoute'] = (req, reply) => {};
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
type MyHandlers = RequestHandler<MySchema, 'GET /hello' | `POST /hello`>;
const myHandlers = (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['Return'] => {};
// if handler is async/await
const myHandlersAsync = async (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['ReturnAsync'] => {};
addSchema(app, {
jsonSchema: {},
service: {
'GET /hello': myHandlers,
'GET /hello2': myHandlers,
},
});
It might be that TS can't infer exact type of complex handler when passed to addSchema so you'll
need to do it manually
addSchema(app, {
jsonSchema: {},
service: {
'GET /hello': myHandlers,
'GET /hello2': myHandlers as RequestHandler<ExtendedSchema, 'GET /hello2'>['AsRoute'],
},
});
This library is using typescript-json-schema with custom
transforms for schema generation. All @tjs annotations can be used to fine-tune schema output
@type can be used to specify end type after using toJSON, toString methods of objects like ObjectID from MongoDB
since we use typejescript-json-schema: all known limitations are also inherited: - Records are not transformed correctly, use { [k: string]: string } instead or hint with @tjs
additionalProperties are set to false by default
{ [k: string]: T } index to explicitly define type for additionalProperties/** @additionalProperties true */ annotation to allow anythingTypeScript
52.5%
JavaScript
46.9%
Better typescript support and runtime safety of fastify handlers
70
stars
237
commits
TypeScript
primary language
Nov 4, 2025
updated
This package adds strong TypeScript support to Fastify request handlers and enforces handlers to have typed schema which is used to validate request params and replies. From this schema it does two things:
request.bodyrequest.headersrequest.querystringrequest.paramsroute.path.params are also inferred and mapped to request.params, it is also not possible to make a typo in schema paramsreply is always based on status, developer won't be able to use plain reply.send() but
forced to explicitly set status first, based on which response type will be inferred@tjs annotations can be used to fine-tune output)
typejescript-json-schema: all known limitations of lib are inherited:
{ [k: string]: string } instead or hint with @tjsnpm i @coobaha/typed-fastify
pnpm i @coobaha/typed-fastify
yarn add @coobaha/typed-fastify
Example of service we want to build
GET / => Hello ($querystring.name || world)
Simple implementation without schema generation will be following
import addSchema, { Schema } from '@coobaha/typed-fastify';
import fastify from 'fastify';
export interface ExampleSchema extends Schema {
paths: {
'GET /': {
request: {
querystring: {
name?: string;
};
};
response: {
200: {
content: string;
};
};
};
};
}
const exampleService: Service<ExampleSchema> = {
'GET /': (req, reply) => {
// typescript will infer correct types for us
const name = req.query.name ?? 'World';
// Calling send directly is not allowed
// reply.send(`Hello ${name}`)
// Calling send with wrong payload will result in an error
// reply.status(200).send(new Date())
return reply.status(200).send(`Hello ${name}`);
},
};
const app = fastify();
addSchema(app, {
// it is strongly recommended to generate json schema to guaruntee runtime validity
jsonSchema: {},
service: exampleService,
});
// Start listening.
app.listen(3000, (err: any) => {
if (err) {
app.log.error(err);
process.exit(1);
}
});
Complex examples can be found typescript tests and in integration.test.ts.
You can generate json schema from your TS types by using typed-fastify-schema or tfs bins
npx tfs gen
tfs gen [files]
Generates json schemas next to corresponding ts files
Positionals:
files glob pattern of files [string] [required]
Options:
--help Show help [boolean]
--version Show version number [boolean]
# it will generate example_schema.gen.json next to file
npx tfs gen example_schema.ts
When schema is generated - just pass it to plugin to have runtime validations 🎉
import jsonSchema from './example_schema.gen.json';
// ...
addSchema(app, {
jsonSchema,
service,
});
Handlers in one object Type inference will work nicely in this case, you just make TS happy and things are working 🥳
Handlers in a different file or separate functions - you will need to hint TS with exact type of handler.
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
const myHandler: RequestHandler<MySchema, 'GET /hello'>['AsRoute'] = (req, reply) => {};
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
const myHandlers: RequestHandler<MySchema, 'GET /hello' | `GET /hello2`>['AsRoute'] = (req, reply) => {};
import { RequestHandler, Schema } from '@coobaha/typed-fastify';
interface MySchema extends Schema {}
type MyHandlers = RequestHandler<MySchema, 'GET /hello' | `POST /hello`>;
const myHandlers = (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['Return'] => {};
// if handler is async/await
const myHandlersAsync = async (req: MyHandlers['Request'], reply: MyHandlers['Reply']): MyHandlers['ReturnAsync'] => {};
addSchema(app, {
jsonSchema: {},
service: {
'GET /hello': myHandlers,
'GET /hello2': myHandlers,
},
});
It might be that TS can't infer exact type of complex handler when passed to addSchema so you'll
need to do it manually
addSchema(app, {
jsonSchema: {},
service: {
'GET /hello': myHandlers,
'GET /hello2': myHandlers as RequestHandler<ExtendedSchema, 'GET /hello2'>['AsRoute'],
},
});
This library is using typescript-json-schema with custom
transforms for schema generation. All @tjs annotations can be used to fine-tune schema output
@type can be used to specify end type after using toJSON, toString methods of objects like ObjectID from MongoDB
since we use typejescript-json-schema: all known limitations are also inherited: - Records are not transformed correctly, use { [k: string]: string } instead or hint with @tjs
additionalProperties are set to false by default
{ [k: string]: T } index to explicitly define type for additionalProperties/** @additionalProperties true */ annotation to allow anythingTypeScript
52.5%
JavaScript
46.9%