Building a Fastify Typescript REST API with type-safe runtime validation
Node.js and Typescript are industry leading technologies. There are plenty of applications built using Node everyday. However, we have noticed that building a type-safe API which uses the same set of types for compile-time and runtime validation can be a challenge for a lot of organisations. This guide aims to prove that managing these types in one place with no duplication is possible, as long as we opt for the right building blocks for our applications.
Requirements
Node.js 16 or higher
Chosen technologies
Typescript — A fully typed superset of Javascript
Fastify — Highly performant API framework with built in validation support
Typebox — JSON Schema Type Builder with Static Type Resolution for TypeScript
Step 1 — Initialise your project and dependencies
Create your project folder and generate a blank
package.json
:
mkdir typescript-rest-api
cd typescript-rest-api
npm init -y
2. Install the relevant dependencies and their associated types:
npm i fastify @fastify/type-provider-typebox @sinclair/typebox
npm i -D typescript @types/node ts-node nodemon
3. Generate a tsconfig.json
file:
npx tsc --init
4. Add some commands to your package.json to run the project:
{
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node index.js",
"dev": "nodemon index.ts"
}
}
Step 2 — Get your API running
Next, we want to get a very simple iteration of our API running in watch mode. This is a quick way to verify that our initial setup is correct. We will move onto adding validation afterwards.
Create a file called
index.ts
in the root of the repoAdd the following lines to the file:
import fastify from 'fastify'
const server = fastify()
server.post('/foo', async (request: any, reply) => {
const { bar, baz } = request.body;
return reply.status(200).send(`You sent a request which contained ${bar} ${baz}!`);
})
server.listen({ port: 8080 }, (err, address) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log(`Server listening at ${address}`)
})
Note: Notice above that we are marking request
as an any
. This is a cardinal sin in Typescript development. We will amend this shortly once we have verified our server is running correctly.
3. Execute the npm run dev
command
4. Check the terminal output, you should see a message saying: Server listening at localhost:8080
5. POST to the /foo
route:
curl --location --request POST 'localhost:8080/foo' \
--header 'Content-Type: application/json' \
--data-raw '{
"bar": true,
"baz": 123
}'
Check that you receive the You sent a request which contained true 123!
Now we have verified that the API is receiving our requests and parsing their bodies.
Step 3 — Spice up your API with compile-time and runtime type checking 🌶🔥
If we wanted to, we could stop at Step 2. Our API is receiving data and we can clearly read it. However, what would we do if someone sent a number instead of a string, or a boolean in the place of an object we expect? Boom. Any code that relies on the request body being in a specific schema (pretty much EVERY line after our route handler) will break if our request is malformed.We need to validate the request body at runtime, and wouldn’t it be nice to know the property types when we are developing the application?Thankfully, Fastify has some incredible integrations which will make this quite straightforward using the Typebox plugin.
Typescript 🤝 Fastify 🤝 Typebox = 😎
Configure Fastify to understand Typebox typings and return all validation errors in one response:
import fastify from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { Static, Type } from "@sinclair/typebox";
const server = fastify({
ajv: {
customOptions: {
allErrors: true,
},
},
}).withTypeProvider<TypeBoxTypeProvider>();
2. Define a Typebox object for our request body and cast it to a static type that Typescript can understand:
const Body = Type.Object({
bar: Type.Boolean(),
baz: Type.Number(),
});
type IBody = Static<typeof Body>;
Note: It’s good practice to define your types in a separate file away from any API specific code, such as in a types
folder. This means that they can be managed and published separately if they need to be. For ease of use in this tutorial, we are writing them inline.
3. Tell Fastify:
To use our Typebox
body
type to validate the request body at runtimeWhat the expected type of the
request.body
object will be at compile time
server.post<{
Body: IBody;
}>(
"/foo",
{
schema: {
body: Body,
},
},
(request, reply) => {
// The request body is now fully typed!
const { bar, baz } = request.body;
return reply.status(200).send(`You sent a request which contained ${bar} ${baz}!`);
}
);
Once we piece the above lines together, we are left with this:
import fastify from "fastify";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { Static, Type } from "@sinclair/typebox";
const server = fastify({
ajv: {
customOptions: {
allErrors: true,
},
},
}).withTypeProvider<TypeBoxTypeProvider>();
const Body = Type.Object({
bar: Type.Boolean(),
baz: Type.Number(),
});
type IBody = Static<typeof Body>;
server.post<{
Body: IBody;
}>(
"/foo",
{
schema: {
body: Body,
},
},
(request, reply) => {
// The request body is now fully typed
const { bar, baz } = request.body;
return reply.status(200).send(`You sent a request which contained ${bar} ${baz}!`);
}
);
server.listen({ port: 8080 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});
Now, when we make a request with an invalid body, we will receive a validation error as a response:
We also know the expected types of our request body while developing, which is a huge benefit of using Typescript:
Summary
In the above tutorial, we have built a type-safe API which uses a single source of truth for both the runtime AND compile time validation. This will drastically reduce the chances of your API accepting and using invalid data.This is a very simple but sturdy foundation to build your systems on. This is a great base we use for a large number of our REST API projects at Logicful.You can take this further by using Typebox and Fastify to also validate query parameters and response bodies as well.
Learning resources
If you have any immediate or future software consultancy needs and want to learn how Logicful ⚛ can help you, get in touch.