Type-Safety & Validation
feTS uses JSON Schema to describe the request parameters and the response body.
It allows us to have:
- Type-safety: In the
handler
, complete type safety for the request parameters and the response body is ensured. You can use a tool like feTS Client that is capable of inferring these types from the router instance. - Request Validation: feTS validates the request parameters and if the request is invalid, it
returns a
400 Bad Request
. - Safe and Fast Serialization: feTS uses the response schema to serialize the response body. Rather than JSON.stringify, it uses fast-json-stringifywhich is twice as fast according to the benchmarks found here.
If you want to use JSON Schema formats like uuid
, you need to register formats using registerFormats
.
import { registerFormats } from 'fets'
registerFormats();
Request
You can type individual parts of the Request
object including JSON body, form data, headers, query
parameters, and URL parameters.
Headers
You can describe the shape of the headers using schemas.request.headers
property. This is useful
for validating API keys or authorization headers.
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'POST',
path: '/todos',
// Define the request body schema
schemas: {
request: {
headers: {
type: 'object',
properties: {
'x-api-key': { type: 'string' }
},
additionalProperties: false,
required: ['x-api-key']
}
}
} ,
handler: async request => {
// This part is fully typed
const apiKey = request.headers.get('x-api-key')
// Would result in TypeScript compilation fail
const wrongHeaderName = request.headers.get('x-api-key-wrong')
// ...
return Response.json({ message: 'ok' })
}
})
Path Parameters
schemas.request.params
can be used to validate the URL parameters like id
inside /todos/:id
:
By default, feTS already extracts and validates path parameters defined in the path. So unless you have some specific format for parameters, you don’t need to define schemas for path parameters..
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'GET',
path: '/todos/:id',
// Define the request body schema
schemas: {
request: {
// This is not needed since feTS already extracts and validates path parameters
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
additionalProperties: false,
required: ['id']
}
// Unless it is `uuid` or some specific format
params: {
type: 'object',
properties: {
id: { type: 'string', format: 'uuid' }
},
additionalProperties: false,
required: ['id']
}
}
} ,
handler: async request => {
// This part is fully typed
const { id } = request.params
// ...
return Response.json({ message: 'ok' })
}
})
Query Parameters
Similar, for query parameters like /todos?limit=10&offset=0
, we can use schemas.request.query
to
define the schema:
import { createRouter, Response } from "fets";
const router = createRouter().route({
method: "GET",
path: "/todos",
// Define the request body schema
schemas: {
request: {
query: {
type: "object",
properties: {
limit: { type: "number" },
offset: { type: "number" },
},
additionalProperties: false,
required: ["limit"],
},
},
} ,
handler: async (request) => {
// This part is fully typed
const { limit, offset } = request.query;
// You can also use `URLSearchParams` API
const limit = request.parsedURL.searchParams.get("limit");
// ...
return Response.json({ message: "ok" });
},
});
JSON Body
We can also specify the JSON body schema by using schemas.request.json
property:
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'POST',
path: '/todos',
// Define the request body schema
schemas: {
request: {
json: {
type: 'object',
properties: {
title: { type: 'string' },
completed: { type: 'boolean' }
},
additionalProperties: false,
required: ['title']
}
}
},
handler: async request => {
// This part is fully typed
const { title, completed } = await request.json()
// ...
return Response.json({ message: 'ok' })
}
})
Form Data / File Uploads
You can also type the request body as multipart/form-data
or application/x-www-form-urlencoded
which are typically used for file uploads.
We use type: string
and format: binary
to define a File
object. The maxLength
and
minLength
of JSON Schema can be used to limit the file size.
Learn more in the OpenAPI docs.
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'POST',
path: '/upload-image',
// Define the request body schema
schemas: {
request: {
formData: {
type: 'object',
properties: {
title: { type: 'string' },
completed: { type: 'boolean' },
image: {
type: 'string',
format: 'binary',
maxLength: 1024 * 1024 * 5 // 5MB
}
},
additionalProperties: false,
required: ['title']
}
}
},
handler: async request => {
// This part is fully typed
const { title, completed, file } = await request.formData()
// ...
return Response.json({ message: 'ok' })
}
})
Response (optional)
The response body can also be typed by the status code. It is highly recommended to explicitly define the status codes.
If you don’t define the response schema, feTS will still infer types from the handler’s return value. However, in this case, you won’t have response types in OpenAPI schema and runtime validation. For example, if you use OpenAPI schema with feTS Client, you won’t have response typings. You will need to infer types directly from the router instead as shown here.
Example:
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'GET',
path: '/todos',
// Define the request body schema
schemas: {
request: {
headers: {
type: 'object',
properties: {
'x-api-key': { type: 'string' }
},
additionalProperties: false,
required: ['x-api-key']
}
},
responses: {
200: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
completed: { type: 'boolean' }
},
additionalProperties: false,
required: ['id', 'title', 'completed']
}
},
401: {
type: 'object',
properties: {
message: { type: 'string' }
},
additionalProperties: false,
required: ['message']
}
}
},
handler: async request => {
const apiKey = request.headers.get('x-api-key')
if (!apiKey) {
return Response.json(
{ message: 'API key is required' },
{
status: 401
}
)
}
const todos = await getTodos({
apiKey
})
// This part is fully typed
return Response.json(todos)
}
})