← The Envert Journal
engineeringMay 28, 2026·10 min read

Kill Your API: Why We Build with Server Functions, Not REST Routes

Tired of bloated REST APIs, endless endpoint debates, and brittle client-server contracts? We are too. Here's why we've moved to server functions for faster, more maintainable, and type-safe full-stack development.

A mechanical keyboard and a glowing monitor displaying code in a dark, neon-lit developer studio.

Building for the web has always been a story of separation. Frontend and backend. Client and server. For decades, the bridge between these two worlds has been the Application Programming Interface (API), and its dominant dialect has been REST (Representational State Transfer).

We spent years building, documenting, and versioning REST APIs. We argued over endpoint naming conventions, HTTP verb semantics, and payload structures. We used tools like Postman and Swagger to define and test this contract. We thought this separation of concerns was a virtue. It's not. It's a tax.

For the vast majority of web applications, the primary consumer of the API is the application's own frontend. The rigid contract of REST, designed for a decoupled, public-facing world, becomes a source of friction, boilerplate, and bugs. We believe there’s a better way, and we've bet our studio's productivity on it: we write server functions instead of REST routes.

The Old Way: A Quick Refresher on REST

Before we tear it down, let's give REST its due. It's the architecture that powered the Cambrian explosion of web services. Based on the simple, elegant constraints of the HTTP protocol, REST gave us a standardized way for different systems to communicate.

Its core principles are sound: a client-server model, statelessness, a cacheable and uniform interface, and the use of resources identified by URLs (e.g., /api/users/123). You use HTTP verbs (GET, POST, PUT, DELETE) to manipulate these resources. This model was a massive improvement over the complex SOAP and XML-RPC protocols that preceded it.

For a public API—one consumed by third-party developers you don't control—REST is still a fine choice. Its explicit, well-documented nature is a feature. But when your frontend and backend are two halves of the same whole, developed by the same team, this explicit contract becomes a chain, not a guardrail. Every change on the backend requires a corresponding change on the frontend. Every new feature requires a new endpoint, which means a new fetch call, new JSON parsing, new error handling, and new state management logic. It's a slow, tedious dance.

Enter Server Functions: The Co-located Future

So what's the alternative? When we say "server functions," we're not just talking about FaaS (Functions as a Service) like AWS Lambda. We're talking about a specific development pattern championed by modern full-stack frameworks like Next.js (Server Actions), SvelteKit, SolidStart, and Remix.

In this model, your server-side logic lives as an exportable function, often in the same file or a file right next to your frontend component. From your client-side code, you don't craft an HTTP request to /api/do-the-thing. You simply import and call the function.

// app/actions.ts (runs only on the server)
'use server';

import { db } from './db';

export async function updateUserProfile(userId: string, data: { name: string; bio: string }) {
  // ... validation logic ...
  const user = await db.user.update({
    where: { id: userId },
    data: { name: data.name, bio: data.bio }
  });
  return { success: true, user };
}
// app/profile/page.tsx (runs on client and/or server)
import { updateUserProfile } from '../actions';

export default function ProfilePage() {
  async function handleSave(formData: FormData) {
    const name = formData.get('name') as string;
    const bio = formData.get('bio') as string;
    // No fetch, no JSON.stringify, no headers.
    // Just call the function.
    await updateUserProfile('user-123', { name, bio });
    // ... handle UI update ...
  }

  return (
    <form action={handleSave}>
      {/* ... form inputs ... */}
      <button type="submit">Save Profile</button>
    </form>
  );
}

This isn't magic. The framework is doing the work under the hood. It creates a private, ad-hoc API endpoint, serializes the arguments, makes the fetch call, and deserializes the response. But from a developer's perspective, you're just calling a function. It's a form of RPC (Remote Procedure Call), but supercharged for the modern web with full TypeScript integration.

The Core Argument: Why We Made the Switch

This shift isn't just a minor syntactic preference. It represents a fundamental improvement in the developer experience that translates directly to a better, faster, more reliable product. Our conviction rests on four pillars.

Unmatched Type Safety, End-to-End

This is the killer feature. With a traditional REST API, your frontend and backend are two separate type systems. You might use TypeScript on both, but there's a void between them. To bridge it, you need to manually keep types in sync, use a schema-first tool like OpenAPI Generator, or validate and infer types on both sides with a library like Zod.

With server functions, this problem vaporizes. Because you're importing the actual function signature, TypeScript knows the exact input arguments and the exact shape of the return value. If you change a parameter on the server function, your frontend code that calls it will immediately show a type error in your editor. There is no gap for type-related bugs to creep in.

This eliminates an entire class of runtime errors, from simple typos in payload keys to complex mismatches in a deeply nested object. The compiler becomes your first and best line of defense, ensuring the client-server contract is never broken. It's like having a permanent, automated integration test running for your entire API surface.

Velocity, Velocity, Velocity

In a startup, speed is survival. Server functions are a massive accelerator. Think of the workflow to add a simple feature with a REST API:

  1. Define the API route and verb (POST /api/posts).
  2. Implement the backend controller logic.
  3. Implement the validation logic.
  4. Write the client-side fetch call.
  5. Handle loading states, error states, and success states on the client.
  6. Manually test the endpoint with Postman or curl.
  7. Manually test the full end-to-end flow in the browser.

With server functions, the workflow is:

  1. Write the server function.
  2. Call it from your component.

That's it. The framework handles the networking, serialization, and basic state management. There's no context switching between api/ and src/ directories, no API client to configure, no separate documentation to maintain. You stay in the flow, thinking about the feature, not the plumbing. This dramatic reduction in cognitive overhead and boilerplate allows a single developer to build full features at a pace that was previously unimaginable.

Slashing Boilerplate and Eliminating API Glue Code

Let's be honest: client-side API interaction code is boring, repetitive, and error-prone. It's almost always a variation of the same try/catch block wrapping a fetch call.

The REST Way:

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

async function createPost(title: string) {
  setIsLoading(true);
  setError(null);
  try {
    const response = await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title }),
    });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const result = await response.json();
    // ... do something with result ...
  } catch (e: any) {
    setError(e.message);
  } finally {
    setIsLoading(false);
  }
}

The Server Function Way:

import { createPost } from './actions';

// Frameworks like Next.js provide hooks to handle pending states
// from form actions, but even without it:
async function handleSubmit(title: string) {
  const { data, error } = await createPost({ title });
  if (error) {
    // ... show error to user
  } else {
    // ... do something with data
  }
}

The server function version is cleaner, more declarative, and leverages the type safety we mentioned earlier. The data and error objects are fully typed. All the messy details of HTTP methods, headers, and body serialization are abstracted away. When you multiply this simplification across hundreds of interactions in a complex app, the savings in code volume and bug potential are enormous.

A More Secure Default

In a REST API, every endpoint is a potential attack vector. You have to be diligent about protecting each one with authentication, authorization, rate limiting, and input validation. It's easy to forget a check on a less-used endpoint, opening a security hole.

Server functions, particularly as implemented with Server Actions in Next.js, flip the model. They aren't general-purpose HTTP endpoints you can call with curl. They are special procedures callable only through the framework's RPC mechanism. They run on the server, in a trusted environment where you can safely access database connections and secret keys without ever exposing them to the client. This colocation of data-fetching with data-mutations on the server-side reduces the surface area for attacks and makes it harder to make security mistakes.

But What About... ? (Addressing the Counterarguments)

This approach isn't a silver bullet. It's a sharp tool, and it's important to know when not to use it.

What about public APIs for third parties? Server functions are for your own frontend. If you need to expose data or functionality to external developers, a well-documented, stable REST (or GraphQL) API is absolutely the right tool for the job. The two can and should coexist. Use server functions for app-internal communication and build a separate, deliberate REST API for public consumption.

Isn't this just RPC, which we abandoned for a reason? Yes, this is a modern take on RPC. The old criticisms of RPC—that it was brittle, opaque, and hard to debug—are solved by today's tooling. TypeScript provides the type safety that was missing, and framework devtools give us visibility into the network calls, making debugging straightforward. We're not repeating the past; we're learning from it.

What about mobile apps or other clients? This is a valid concern. If you know from day one that you'll have a web app, an iOS app, and an Android app all sharing the exact same backend logic, building a single REST or GraphQL API might be more efficient. However, you can also expose a dedicated API for your mobile apps while still using server functions for your web app to maximize development speed there. Or, even better, use a tool like tRPC which allows you to share the typed server function pattern across a React Native mobile app and a Next.js web app.

A Practical Example: Building a Form

Let's see this in action with the most common task on the web: submitting a form.

The task: A simple newsletter signup form with an email field.

The REST API Way

  1. Backend: Create a file pages/api/subscribe.ts. Write code to handle a POST request, parse the JSON body, validate the email, and save it to a database. Handle errors and send back a JSON response.
  2. Frontend: Create a component with a form. Add an onSubmit handler. Use preventDefault(). Manage isLoading and error state. Inside the handler, fetch('/api/subscribe', ...) with the correct method, headers, and stringified body. Await the response, parse it, and update the UI.

The Server Function Way (with Next.js App Router)

  1. Backend/Server Action: Create a file app/actions.ts. Export an async function subscribeToNewsletter(formData: FormData). Inside, get the email from formData, validate it, and save it to a database. Return a status object.
  2. Frontend: Create a server component with a <form>. Set the action prop to your imported server function: <form action={subscribeToNewsletter}>. That's it. The framework handles the rest. For progressive enhancement, it even works without JavaScript.

Here's what you're cutting out with the server function approach:

  • No separate API route file.
  • No manual fetch call.
  • No JSON.stringify or JSON.parse.
  • No preventDefault().
  • No manual isLoading state management (frameworks often provide this out of the box with hooks like useFormStatus).

It's not just less code; it's a fundamentally simpler, more direct mental model.

The Founder's Bottom Line

As a founder, you care about one thing: creating value for your customers, faster and better than your competitors. This is not just an academic engineering debate; it's a strategic business decision.

Adopting server functions means:

  • Faster Time-to-Market: Your team will build and ship features in a fraction of the time it takes with a traditional REST architecture. Less plumbing means more focus on the product.
  • Reduced Bugs & Higher Quality: End-to-end type safety eliminates an entire category of common bugs, leading to a more stable product and happier users.
  • Lower Development & Maintenance Costs: Simpler codebases with less boilerplate are cheaper to build and easier to maintain. Onboarding new developers is faster because there's no complex API client or backend contract to learn.
  • Tighter, More Agile Teams: This model blurs the line between frontend and backend, allowing small, full-stack teams (or even a single developer) to be incredibly productive.

The era of the laborious, hand-cranked internal API is over. The bridge between the client and server shouldn't be a chasm spanned by a rickety rope bridge of fetch calls. It should be a seamless, type-safe, and incredibly fast superhighway. For us, that highway is paved with server functions.

Frequently asked questions

Is this going to lock me into a specific framework like Next.js?+

To some extent, yes. This pattern is tightly integrated with modern full-stack frameworks. However, the productivity gains are so significant that we see it as a worthwhile trade-off. The core concepts are also becoming standard, so moving between frameworks like Next.js, SvelteKit, or SolidStart is more feasible than a complete architectural rewrite.

Will my frontend developers need to become backend experts?+

Not really. They need to be comfortable writing JavaScript/TypeScript functions, but they won't need to manage servers, databases, or complex infrastructure. The framework abstracts away the difficult parts of the backend, allowing them to write server-side logic in a familiar environment.

How does this affect hiring? Is it harder to find developers with these skills?+

On the contrary, it can make hiring easier. This is the direction the industry is heading, and top-tier full-stack developers are excited to work with these modern tools. You'll be attracting talent that is passionate about developer experience and productivity, which is a huge plus for any startup.

What if we need to build a mobile app later? Will we have to build a separate REST API anyway?+

This is a key consideration. If you need to support a native mobile app, you have two great options: build a dedicated REST/GraphQL API for it, or use a technology like tRPC that extends this same type-safe pattern to React Native. This way, you don't have to abandon the model; you just adapt your strategy.

Isn't it riskier to have frontend and backend code so close together?+

It's actually safer by default. The framework clearly delineates what runs on the server (marked with `'use server'`) and what runs on the client. Critical logic and secrets never leave the server, reducing your application's attack surface compared to exposing numerous manual API endpoints.

#server functions#rest#api#full-stack#typescript#rpc

Ready to ship your next product?

Free 30-minute call. We'll scope your build, name the smallest billable wedge, and tell you honestly if we're the right team.

4.9/5 · 200+ products shipped
90-day MVP guarantee