惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Blog — PlanetScale
Blog — PlanetScale
小众软件
小众软件
F
Fortinet All Blogs
博客园 - 叶小钗
博客园_首页
D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
U
Unit 42
爱范儿
爱范儿
aimingoo的专栏
aimingoo的专栏
博客园 - Franky
Martin Fowler
Martin Fowler
酷 壳 – CoolShell
酷 壳 – CoolShell
The Cloudflare Blog
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
IT之家
IT之家
M
MIT News - Artificial intelligence
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
The server boundary disappears
Bryan MacLee · 2026-04-29 · via DEV Community

authored by claude, rubber stamped by Bryan MacLee

TL;DR: Stop writing API routes. The compiler does it.

Every framework dev has shipped a bug where they thought a function ran on the server but it didn't, or thought it ran on the client but it didn't. I am not an experienced framework developer. I can hobble through React if I HAVE TO. But across about twenty compiler attempts the same shape kept showing up: the compiler should know which side of the wire each function is on, because the wire is part of the program.

I'm obsessed with performance. I'm also a believer in "do it right, the first time, even if it takes more time." The server boundary is a place where most languages do neither, and the runtime pays for both. So this is the second of six features the browser-language overview piece promised to unpack later. The boundary, in detail.

What "shipping a typed POST endpoint" actually costs

Pick a framework. Next, Remix, Express plus a React frontend, doesn't matter. The minimum table-stakes for a single server endpoint that takes some data, validates it, persists it, and returns a typed response looks like this:

On the server:

// app/api/orders/route.ts
import { z } from "zod";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";

const SubmitOrderInput = z.object({
  items: z.array(z.object({
    productId: z.string().uuid(),
    qty: z.number().int().min(1).max(99),
  })),
});

const SubmitOrderOutput = z.object({
  orderId: z.string().uuid(),
  total: z.number(),
});
type SubmitOrderOutput = z.infer<typeof SubmitOrderOutput>;

export async function POST(req: Request) {
  const session = await auth(req);
  if (!session) return new Response("Unauthorized", { status: 401 });

  let body: unknown;
  try { body = await req.json(); }
  catch { return new Response("Bad JSON", { status: 400 }); }

  const parsed = SubmitOrderInput.safeParse(body);
  if (!parsed.success) {
    return new Response(JSON.stringify(parsed.error), { status: 400 });
  }

  // ... actual business logic finally starts here ...
  const order = await db.orders.insert(...);
  return Response.json({ orderId: order.id, total: order.total });
}

Enter fullscreen mode Exit fullscreen mode

On the client:

// app/cart/page.tsx
type SubmitOrderInput = { items: { productId: string; qty: number }[] };
type SubmitOrderOutput = { orderId: string; total: number };

async function submitOrder(input: SubmitOrderInput): Promise<SubmitOrderOutput> {
  const res = await fetch("/api/orders", {
    method: "POST",
    headers: { "content-type": "application/json", "x-csrf-token": getToken() },
    body: JSON.stringify(input),
  });
  if (!res.ok) throw new Error(await res.text());
  return res.json() as Promise<SubmitOrderOutput>;
}

Enter fullscreen mode Exit fullscreen mode

That's the minimum. No retry. No timeout. No proper error type. No loading state. The shapes are typed twice, once with zod and once with hand-written TS. The CSRF token plumbing is manual. If you change the input shape on the server and forget to update the client TS type, nothing fails until production traffic hits it.

This is the seam. It is the most expensive seam in the application. It is also the seam that no framework can close, because the framework doesn't own both sides of it.

The same feature in scrml

< db src="./app.db" tables="orders,order_items">

server fn submitOrder(items: List<Item>(@length > 0 && @length < 100)) -> OrderResult {
    let total = items.sum(it => it.price * it.qty)
    let orderId = ?{`INSERT INTO orders (total) VALUES (${total}) RETURNING id`}.get().id
    items.forEach(it => {
        ?{`INSERT INTO order_items (order_id, product_id, qty) VALUES (${orderId}, ${it.productId}, ${it.qty})`}.run()
    })
    return OrderResult { orderId: orderId, total: total }
}
</>

Enter fullscreen mode Exit fullscreen mode

And the call site, anywhere in the same file or another .scrml file in the project:

let result = submitOrder(@cart.items)

Enter fullscreen mode Exit fullscreen mode

That's the entire feature. Both halves.

What the compiler did with server fn:

  1. Generated a server-side route handler. Route name is compiler-internal; you don't reference it.
  2. Generated the client-side fetch call that invokes the route, with arg serialization, response deserialization, and await insertion. The developer SHALL NOT write JSON.stringify, JSON.parse, or fetch to consume server function return values (§12.5).
  3. Type-checked the call site. submitOrder(@cart.items) checks the argument shape against the server fn signature in the same compile pass. There is no client-side type SubmitOrderInput declaration to drift.
  4. Emitted the function body to the server output only. The client gets a fetch stub.
  5. Enforced the predicate at the server boundary. (@length > 0 && @length < 100) is checked at function entry, on the server, before any database write. The check runs even if the client already validated (§53.9.4).

What the compiler refuses

Six refusals, every one of them a real diagnostic with an E-code you can look up.

1. Reading a server-only field on the client. (E-PROTECT-001)
If < db> declares protect="passwordHash", then passwordHash does not exist on the client type. A client-side function trying to read it is a compile error, not a runtime exposure.

2. Code that accesses a protected field but might run client-side. (E-PROTECT-002)
The compiler verifies at compile time that no function accessing protected fields executes on the client. Any code path that could route to the client and touches a protected field fails compile.

3. A server fn calling a client-only fn. (E-ROUTE-002)
If a server fn transitively calls a function that touches the DOM or reads a client-only derived value, compile fails with the call chain printed. The error message names the server function, the client-only callee, and the path between them, then suggests three resolutions: extract a pure function, duplicate the logic, or re-evaluate the classification.

4. A non-serializable return type from a server fn. (E-ROUTE-003)
Try to return a function, a DOM node, or a class instance from server fn and the compile fails. The wire is JSON; the type system enforces it.

5. A client-local @var used as a bound parameter in INSERT, UPDATE, or DELETE outside a server fn. (E-AUTH-001)
The compiler refuses to silently persist client-local state. The error message tells the developer to pass the value to a server function first.

6. A predicate violation at the boundary. (E-CONTRACT-001 at compile time, E-CONTRACT-001-RT at the boundary.)
If the compiler can prove a literal violates a predicate, the build fails. If the value is only known at runtime, a server-side boundary check fires before any business logic runs and rejects the request.

That's six refusals. Every one of them is a type-system answer to a question that, in framework land, is "hope your tests catch it."

What gets generated for free

Beyond refusing the wrong things, the compiler also generates the things you would have written by hand:

  • The fetch stub. Argument serialization, response deserialization, automatic await. No JSON.stringify. No JSON.parse. No manual await.
  • The route handler. With its name as a compiler-internal detail you never see.
  • CSRF plumbing, when <program csrf="on"> is set. A token-mint server fn, a <meta name="csrf-token"> injection in the generated HTML, a request interceptor that adds the X-CSRF-Token header to every state-mutating request, and a server-side validator that returns 403 if the token is missing or invalid (§39.2.3).
  • Predicate validation at the boundary. Inline predicate constraints on server function parameters are enforced server-side, before any database write or business logic, independently of any client-side check. A server function's parameter constraint cannot be bypassed by raw HTTP requests (§53.9.4).
  • Async parallelization. Independent server calls in the same function body are parallelized in generated code; dependent ones are sequenced. The developer writes flat synchronous-looking code; the compiler emits Promise.all and await correctly (§13.2).

The compiler is the dev's best friend. That phrase comes up a lot in my notes. This is what it means in practice. Every line of the framework boilerplate above is moved into the compiler, where it cannot drift, cannot be skipped under deadline pressure, and cannot be wrong without the build failing.

What this kills

  • The app/api/ directory. There aren't any route files. There are functions.
  • Hand-written fetch wrappers. There is no apiClient.ts.
  • tRPC and OpenAPI codegen steps. The type goes through the boundary natively because the compiler owns both sides.
  • Zod-on-the-wire. Inline predicates are the type. A schema file isn't a separate artifact. Why would anyone bring zod into a scrml project?
  • Type-drift bugs that ship to prod. The client and server agree on the shape because there is one declaration.

What is still real

Server-side concerns don't disappear. They just stop being plumbing.

  • Auth. Still your job. The server annotation is described in the spec as a security escape hatch precisely because compile-time inference of "what touches protected data" is not always sufficient on its own (§11.4). Annotate auth-touching functions with server explicitly.
  • Rate limiting. A <program ratelimit="100/min"> attribute generates a sliding-window limiter (§39.2.4). Tune the rate to your business; the mechanism is built in.
  • Input validation against business rules. Predicates handle shape and range. Business rules ("this user can submit at most 3 of these per day") are still business logic. They live inside the server fn. They benefit from running where the data lives.

The line between "plumbing the framework forced you to write" and "actual business logic" gets a lot brighter when one side of it is gone.

The deeper claim

A reactive system that wires its dependencies at compile time does no work at runtime to figure out what to update. A query that batches itself at compile time doesn't need a DataLoader. A boundary that is enforced at compile time doesn't need a validator on the wire.

The runtime does less because the compiler did more. The seam between client and server stops being a place where bugs live and starts being a place where the type system has the most leverage. That is the design. A little short of perfect is still pretty awesome.

Further reading