API’s Explained | HTTP vs RPC

Published:
October 23, 2025
October 23, 2025
Updated:
October 23, 2025

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.”

Here we’re “fetching a resource” at /api/users/123/subscription.

// 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"

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 functiongetSubscriptionStatuswith parameter 123.”

// 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"

The code looks almost identical, but the philosophy is different.

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

HTTP (REST/GraphQL) vs RPC (tRPC, better-call)
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"

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?

Written by...
Mike Karan

I've been a full-stack web developer for 8+ years! Co-host of the HTML All The Things Podcast. I love all things technology.

More to Read...