@farbenmeer/tapi
v0.6.1
Published
This library makes it possible to build a Rest API including a fully typed client without a compile step.
Keywords
Readme
TApi
This library makes it possible to build a Rest API including a fully typed client without a compile step.
State of this library
Very much alpha. I just came up with this idea. Currently only supports GET and POST requests.
Plans
- Support more HTTP methods
- Add function to automatically generate OpenAPI specs
- Add support for server-clients for use on the server that call the handler functions directly instead of making a fetch request
Inspiration
Very much TRPC. It's quite similar but with a REST paradigm so the API is actually usable for other clients.
Setup
npm install @farbenmeer/tapi
yarn add @farbenmeer/tapi
pnpm add @farbenmeer/tapi
bun add @farbenmeer/tapiCreate a file (conventionally called api.ts) with your API definition:
import { defineApi } from "@farbenmeer/tapi/server"
export const api = defineApi()Set up a route to handle the requests. This depends on your framework. For Next.js it would be app/api/[...tapi]/route.ts:
import { api } from "api"
import { createRequestHandler } from "@farbenmeer/tapi/server"
const handler = createRequestHandler(api, {
basePath: "/api"
})
export const GET = handler
export const POST = handler
export const PUT = handler
export const DELETE = handler
export const PATCH = handlerCreate another file (conventionally called client.ts) with your client definition:
import type { api } from "./api"
import { defineFetchClient } from "@farbenmeer/tapi/client"
export const client = defineFetchClient<typeof api>(apiUrl)where apiUrl is the base URL of your API, usually something like https://example.com/api.
Adding Routes
Define your first route as a file, in this example api/books.ts:
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
export const GET = defineHandler({ authorize: () => true }, async () => {
return TResponse.json([
{ id: 1, title: "TApi for Dummies" },
{ id: 2, title: "Advanced TApi" }
])
})extends api.ts:
import { defineApi } from "@farbenmeer/tapi/server"
export const api = defineApi()
.route("/books", import("./api/books"))Authorization
The authorize argument in defineHandler allows you to authorize routes based on request headers and other request data. The authorize function receives a TRequest object that extends the standard Request, giving you access to headers, URL parameters, and query parameters for authorization decisions.
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
export const GET = defineHandler({
authorize: (req) => {
const authHeader = req.headers.get('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
throw new Error('Unauthorized: Missing or invalid token')
}
const token = authHeader.slice(7) // Remove 'Bearer ' prefix
// Validate token and return user data
return validateTokenAndGetUser(token)
}
}, async (req) => {
// req.auth now contains the data returned from authorize
const user = req.auth()
return TResponse.json({ message: `Hello ${user.name}` })
})The authorize function can return any data type, which will then be available as req.auth() in your handler. This is useful for passing user information, permissions, or other authorization context to your route handlers.
Using the client
In your client-side javascript code, you can use the client to make requests to your API. For example:
import { client } from "client"
async function fetchBooks() {
const books = await client.books.get()
console.log(books)
}
fetchBooks()Dynamic Paths
Define a route with a dynamic path parameter using the params option for defineHandler, for example /api/book.ts
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
import { z } from "zod/v4"
export const GET = defineHandler({
authorize: () => true,
params: { id: z.string() }
}, async (req) => {
return TResponse.json({
id: req.params().id,
title: `Book with id ${req.params().id}`
})
})extends api.ts:
import { defineApi } from "@farbenmeer/tapi/server"
export const api = defineApi()
.route("/books", import("./api/books"))
.route("/books/:id", import("./api/book"))call the route on the client as
import { client } from "client"
async function fetchBook(id: string) {
const book = await client.books[id].get()
console.log(book)
}
fetchBook('1')Query Parameters
Define a route with query parameters using the query option for defineHandler, for example /api/search.ts
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
import { z } from "zod/v4"
export const GET = defineHandler({
authorize: () => true,
query: { q: z.string() }
}, async (req) => {
return TResponse.json({
query: req.query().q,
results: []
})
})extends api.ts:
import { defineApi } from "@farbenmeer/tapi/server"
export const api = defineApi()
.route("/books", import("./api/books"))
.route("/books/:id", import("./api/book"))
.route("/search", import("./api/search"))call the route on the client as
import { client } from "client"
async function searchBooks(q: string) {
const books = await client.search.get({ q })
console.log(books)
}
searchBooks('TApi')Post Requests
Define a POST-route, for example to add a book in api/books.ts:
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
import { z } from "zod/v4"
/* export const GET = ... */
export const POST = defineHandler({
authorize: () => true,
body: z.object({ title: z.string() }),
}, async (req) => {
const { title } = await req.data()
const book = await createThisBook({ title })
return TResponse.json(book)
})and call it as either:
import { client } from "client"
async function addBook(title: string) {
const book = await client.books.post({ title })
console.log(book)
}
addBook('TApi')or with formData:
import { client } from "client"
async function addBook(title: string) {
const formData = new FormData()
formData.set('title', title)
const book = await client.books.post(formData)
console.log(book)
}
addBook('TApi')Wildcard Routes
Use wildcards to match arbitrary paths:
// api/files.ts
import { defineHandler, TResponse } from "@farbenmeer/tapi/server"
import { z } from "zod/v4"
export const GET = defineHandler({
authorize: () => true,
params: { path: z.string() }
}, async (req) => {
const filePath = req.params().path
// filePath = "documents/report.pdf" for /api/files/documents/report.pdf
const file = await getFile(filePath)
return TResponse.json(file)
})Define the route in api.ts:
export const api = defineApi()
.route("/files/*path", import("./api/files"))Call it from the client:
const file = await client.files["documents/report.pdf"].get()Note: Wildcards (* or *name) match everything including slashes and must come at the end of the path. Use *name to capture the matched portion as a parameter.
which is particularly useful with react forms:
<form action={client.books.post}>
<input name="title" />
</form>