T
TanStack7mo ago
conscious-sapphire

Shared API Route or Server Functions across projects (OpenAPI?) - no monorepo

Is there a way to effectively share the type definitions of either API routes or server functions so they can be used in other projects? For example, I have a mobile app (Expo) that I'd like to make calls to my TanStack Start app.
4 Replies
foreign-sapphire
foreign-sapphire7mo ago
I can't find it anywhere in the doc but Tanner announced createServerFnClient a while ago. https://bsky.app/profile/tannerlinsley.com/post/3ldunfdfrp725
Tanner Linsley (@tannerlinsley.com)
WAT HAZ I DUN @TANSTACK.COM START
Likes
115
From Tanner Linsley (@tannerlinsley.com)
Bluesky
conscious-sapphire
conscious-sapphireOP7mo ago
Not sure that would be statically typed. but at least accessible or maybe there is some magic i'm missing looks like someone was asking for it tho
foreign-sapphire
foreign-sapphire7mo ago
Not sure that would be statically typed.
Yeah I wondered the same thing, still patiently waiting for upcoming docs.
conscious-sapphire
conscious-sapphireOP6mo ago
createServerFnClient <-- just saying this to tag this thread tried wrapping server functions to make some nice OpenAPI.. problem is server functions seem to be part of SSR, so they don't exist always so can't be iterated..
export const helloWorld = createOpenAPIServerFn({
method: 'GET',
inputSchema: Type.Object({
name: Type.Optional(Type.String()),
}),
outputSchema: Type.Object({
message: Type.String(),
})
})
export const helloWorld = createOpenAPIServerFn({
method: 'GET',
inputSchema: Type.Object({
name: Type.Optional(Type.String()),
}),
outputSchema: Type.Object({
message: Type.String(),
})
})
simple testing implementation
import { createServerFn } from '@tanstack/react-start';
import { TSchema } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';


export type HTTPMethod = 'GET' | 'POST' // figure out how to use the real one

export interface OpenAPIServerFnOptions {
method?: HTTPMethod;
inputSchema: TSchema;
outputSchema: TSchema;
}

export interface OpenAPIServerFnMetadata {
method: HTTPMethod;
inputSchema: TSchema;
outputSchema: TSchema;
url: string;
fn?: Function
}

// A registry to hold metadata for all our server functions (server-only)
const openAPIServerFnRegistry: OpenAPIServerFnMetadata[] = [];


export function createOpenAPIServerFn<Input, Output>(
options: OpenAPIServerFnOptions
) {
const check = TypeCompiler.Compile(options.inputSchema);
const tbValidator = (data: any): Input => {
if (!check.Check(data)) {
const errors = Array.from(check.Errors(data)).map(e => e.message).join(', ');
throw new Error(`Validation failed: ${errors}`);
}
return data as Input;
};

// this will error.. some wizardry
// ^^^^^^^^^^^^^^ createServerFn must be called with a "handler" property!
const serverFn = createServerFn({ method: options.method }).validator(tbValidator);

// Build our metadata.
const metadata: OpenAPIServerFnMetadata = {
method: options.method || "POST",
inputSchema: options.inputSchema,
outputSchema: options.outputSchema,
url: (serverFn as any).url, // HACK: Assumes createServerFn attaches a url property really we need to attach a url

};

if (typeof window === 'undefined') {
openAPIServerFnRegistry.push(metadata);
}

return serverFn;
}
import { createServerFn } from '@tanstack/react-start';
import { TSchema } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';


export type HTTPMethod = 'GET' | 'POST' // figure out how to use the real one

export interface OpenAPIServerFnOptions {
method?: HTTPMethod;
inputSchema: TSchema;
outputSchema: TSchema;
}

export interface OpenAPIServerFnMetadata {
method: HTTPMethod;
inputSchema: TSchema;
outputSchema: TSchema;
url: string;
fn?: Function
}

// A registry to hold metadata for all our server functions (server-only)
const openAPIServerFnRegistry: OpenAPIServerFnMetadata[] = [];


export function createOpenAPIServerFn<Input, Output>(
options: OpenAPIServerFnOptions
) {
const check = TypeCompiler.Compile(options.inputSchema);
const tbValidator = (data: any): Input => {
if (!check.Check(data)) {
const errors = Array.from(check.Errors(data)).map(e => e.message).join(', ');
throw new Error(`Validation failed: ${errors}`);
}
return data as Input;
};

// this will error.. some wizardry
// ^^^^^^^^^^^^^^ createServerFn must be called with a "handler" property!
const serverFn = createServerFn({ method: options.method }).validator(tbValidator);

// Build our metadata.
const metadata: OpenAPIServerFnMetadata = {
method: options.method || "POST",
inputSchema: options.inputSchema,
outputSchema: options.outputSchema,
url: (serverFn as any).url, // HACK: Assumes createServerFn attaches a url property really we need to attach a url

};

if (typeof window === 'undefined') {
openAPIServerFnRegistry.push(metadata);
}

return serverFn;
}

Did you find this page helpful?