Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

Server-first Quickstart

Learn how to use rest-rpc with server-first approach

While rest-rpc is primarily designed for contract-first development, it can also be used server-first. In server-first development, the client is derived from the server implementation automatically.

This is useful when:

  • You don’t want to create a separate package for the API contract
  • Server implementation should be the single source of truth
  • You want the least amount of boilerplate possible
  • You are more used to thinking in terms of server routes than API contracts

This guide mirrors the Quickstart guide, but with a server-first approach.

Install packages

Add the core package, a schema library, and the adapter for the server framework you want to use.

Define and Implement the server route

Write server handlers using any Fetch or Node HTTP adapter compatible framework.

Call it from the client

Create a typed fetch client or TanStack Query helpers from the same contract.

Install Packages

This guide uses Zod and Node.js HTTP by default. The validation library can be replaced with any library implementing the Standard Schema or a built-in type-only schema.

pnpm add @rest-rpc/core @rest-rpc/node zod
npm install @rest-rpc/core @rest-rpc/node zod
yarn add @rest-rpc/core @rest-rpc/node zod
bun add @rest-rpc/core @rest-rpc/node zod

The server-first approach is currently implemented to @rest-rpc/node and @rest-rpc/fetch adapters. These adapters can generally be used inside any framework that is built on Node.js HTTP or Fetch API.

For the TanStack Query client example, also install:

pnpm add @rest-rpc/tanstack-query @tanstack/react-query

Define routes on the server

import { createServer } from "node:http";
import { route, createRouteHandler } from "@rest-rpc/node";
import { z } from "zod";

export const routes = {
	todos: {
		create: route
			.post("/todos")
			.body(z.object({ title: z.string().min(1) }))
			.handler(({ title }) => ({
				status: 201,
				body: { id: crypto.randomUUID(), title, completed: false },
			})),
	},
};

const handle = createRouteHandler(routes);

const server = createServer(async (request, response) => {
	const { matched } = await handle(request, response);
	if (!matched) {
		response.writeHead(404).end("Not found");
	}
});

server.listen(3000);
import { route, createRouteHandler } from "@rest-rpc/fetch";
import { z } from "zod";

export const routes = {
	todos: {
		create: route
			.post("/todos")
			.body(z.object({ title: z.string().min(1) }))
			.handler(async ({ title }) => ({
				status: 201 as const,
				body: { id: crypto.randomUUID(), title, completed: false },
			})),
	},
};

const handle = createRouteHandler(routes);

export default {
	async fetch(request: Request) {
		const result = await handle(request);
		return result.matched
			? result.response
			: new Response("Not found", { status: 404 as const });
	},
};

Create a Client

import { initClient } from "@rest-rpc/core";
import type { routes } from "./server";

const client = initClient<typeof routes>({
	baseUrl: "https://api.example.com",
});

const response = await client.$post("/todos", {
	body: { title: "Ship v1" },
});
const todo = response.body;
import { createTanstackQueryHelpers } from "@rest-rpc/tanstack-query";
import { useMutation } from "@tanstack/react-query";
import type { routes } from "./server";

const tq = createTanstackQueryHelpers<typeof routes>({
	baseUrl: "http://localhost:3000",
});

const createTodo = useMutation(
	tq.$post("/todos").mutationOptions({
		onSuccess(response) {
			console.log(response.body.id);
		},
	}),
);

createTodo.mutate({
	title: "Ship v1",
});

Next Steps

Was this page helpful?