This content originally appeared on DEV Community and was authored by Mikhail Karan
Introduction
Every modern web app has to connect a client (browser, mobile app, desktop app) to a server.
Why? To do things like:
- Check if a user is logged in.
- Get their subscription status.
- Update their profile picture.
- Process a payment.
These requests are how applications feel “alive”, the client asks the server for information, and the server responds.
Two common ways to make those requests are:
- HTTP APIs (like REST or GraphQL).
- Remote Procedure Calls (RPC).
At first glance, they both look similar: you send data and get data back. But their design philosophy and developer experience are quite different. Let’s walk through both, with real-world examples.
What is HTTP?
HTTP (HyperText Transfer Protocol) is the language of the web. It works by thinking in resources (nouns), each exposed at a URL.
- Example: “Give me the subscription status of user 123.”
// REST-style HTTP call with fetch
async function getSubscriptionStatus(userId: string) {
const res = await fetch(`/api/users/${userId}/subscription`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
return res.json() as Promise<{ status: "active" | "inactive" }>;
}
const subscription = await getSubscriptionStatus("123");
console.log(subscription.status); // "active"
Here we’re “fetching a resource” at /api/users/123/subscription.
What is RPC?
RPC (Remote Procedure Call) is designed around actions (functions/methods) instead of URLs.
You don’t think “hit this endpoint.” Instead you think: “Call the function getSubscriptionStatus with parameter 123.”
The code looks almost identical, but the philosophy is different.
// Example RPC call (JSON-RPC-like)
async function callRPC(method: string, params: unknown) {
const res = await fetch("/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method, params }),
});
return res.json();
}
// Looks like a function call instead of a URL
const subscription = await callRPC("getSubscriptionStatus", { userId: "123" });
console.log(subscription.result.status); // "active"
Notice how the method name (getSubscriptionStatus) is the function, and the params are the arguments. That’s the “functions and methods” idea in plain English.
Key Differences: RPC vs HTTP
| Aspect | HTTP (REST/GraphQL) | RPC (tRPC, better-call) |
|---|---|---|
| How you think | “Get the /users/123/subscription resource” |
“Call getSubscriptionStatus(123)” |
| Interface | Endpoints & resources (nouns) | Functions & methods (verbs) |
| Type safety | Optional (OpenAPI, GraphQL) | Strong with TypeScript bindings |
| Standardization | Universally understood | Framework-specific |
| DX | Boilerplate-heavy | Feels like local function calls |
Example with tRPC
Here’s how the same thing looks in tRPC:
// server.ts
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const appRouter = t.router({
getSubscriptionStatus: t.procedure
.input((id: string) => id)
.query(({ input }) => {
// simulate DB lookup
return { status: "active" };
}),
});
export type AppRouter = typeof appRouter;
// client.ts
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server";
const client = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url: "/trpc" })],
});
// Looks like a local function call
const subscription = await client.getSubscriptionStatus.query("123");
console.log(subscription.status); // "active"
Here you literally “call the function” on the client, and it automatically makes the server request. That’s the DX benefit of RPC.
Example with better-auth (better-call)
better-auth uses better-call (an RPC package) under the hood. Instead of manually hitting /api/auth/session, you call a method.
// auth.ts
import { createAuthClient } from "better-auth";
const auth = createAuthClient({ baseURL: "/api/auth" });
// Get the current session
const session = await auth.session.get();
console.log(session?.user.name);
// Log in
await auth.signIn.email({ email: "test@example.com", password: "secret" });
Again: no manual URLs, just functions you can call.
Pros and Cons
RPC
- ✅ Strong typing, great DX in TypeScript
- ✅ Feels like local function calls
- ❌ Less standardized, harder to expose publicly
HTTP
- ✅ Universally understood, great for public APIs
- ✅ Mature ecosystem (docs, caching, gateways)
- ❌ More boilerplate
- ❌ Weaker type inference without extra tooling
When to Use Which
- Use RPC when building internal apps or monorepos where both client and server are in TypeScript. (tRPC, better-call).
- Use HTTP (REST/GraphQL) when building public APIs or needing wide interoperability with non-TypeScript clients.
Closing Thoughts
Both RPC and HTTP let clients talk to servers, whether you’re checking subscription status, logging in, or updating user data. The difference is how you think about those calls:
- HTTP → “resources and endpoints.”
- RPC → “functions and methods.”
For internal, TypeScript-heavy projects, RPC can feel magical. For external APIs, HTTP remains the practical standard.
Do you want me to expand the beginner-friendly narrative even more (like including a running story example: “Alice logs into a SaaS app and checks her subscription” across both approaches), or keep it in this more technical-but-accessible style?
This content originally appeared on DEV Community and was authored by Mikhail Karan
Mikhail Karan | Sciencx (2025-10-24T13:56:31+00:00) API’s Explained | HTTP vs RPC. Retrieved from https://www.scien.cx/2025/10/24/apis-explained-http-vs-rpc/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.