Type-Safety & Validation
feTS uses JSON Schema (opens in a new tab) to describe the request parameters and the response body.
This allows us to have;
- Type-safety: In the
handler
, you have full type-safety for the request parameters and the response body. Also you can use a client like feTS Client that can infer these types from the router instance itself. - Request Validation: feTS validates the request parameters and returns
400 Bad Request
if the request is invalid. It uses AJV (opens in a new tab) under the hood. - Safe and Fast Serialization: feTS uses the response schema to serialize the response body. It
uses fast-json-stringify (opens in a new tab) under the hood instead
of
JSON.stringify
, and this is 2x faster according to the benchmarks can be found here (opens in a new tab).
Setting up runtime validations with AJV
If you don't use Zod schemas and want to have a strict runtime validation with JSON schemas. You need to configure AJV plugin for feTS. AJV is opt-in because some environments like Cloudflare Workers do not play well with AJV.
import { createRouter, useAjv } from 'fets'
const router = createRouter({
plugins: [useAjv()]
})
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 the API key or the authorization header.
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']
}
}
} as const,
handler: async request => {
// This part is fully typed
const apiKey = request.headers.get('x-api-key')
// Would fail on TypeScript compilation
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
.
import { createRouter, Response } from 'fets'
const router = createRouter().route({
method: 'get',
path: '/todos/:id',
// Define the request body schema
schemas: {
request: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
},
additionalProperties: false,
required: ['id']
}
}
} as const,
handler: async request => {
// This part is fully typed
const { id } = request.params
// ...
return Response.json({ message: 'ok' })
}
})
Query Parameters
import { createRouter, Response } from 'fets'
const router = createRouter().addRoute({
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']
},
}
} as const,
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
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']
}
}
} as const,
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
usually used for file uploads.
We use type: string
and format: binary
to define a File
object. We can also use maxLength
and minLength
of JSON Schema to limit the file size.
Learn more from the OpenAPI docs (opens in a new tab)
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']
}
}
} as const,
handler: async request => {
// This part is fully typed
const { title, completed, file } = await request.formData()
// ...
return Response.json({ message: 'ok' })
}
})
Response (optional)
You can also type the response body by the status code. We strongly recommend 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. But in this case, you won't have response types in OpenAPI schema and runtime validation. Let's say if you use OpenAPI schema with feTS Client, you won't have response typings. You have to infer types directly from router instead as shown here.
import { createRouter, Response } from 'fets'
const router = createRouter().addRoute({
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']
}
}
} as const,
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)
}
})