tarek.kharsa

home / lessons / opencode · lesson 5

API designOpenCode · lesson 5 of 82 min read

One API, even in-process

The embedded SDK sends real requests to the real router, without a network.

Problem

An embedded SDK that calls internals directly slowly drifts from the HTTP API: different validation, fields and errors.

Idea

Send embedded calls through the real router with an in-memory fetch, so both modes share one API.

Most tools with both a server and an embeddable SDK end up with two APIs. The HTTP one is used by remote clients. The "local" one calls internal functions directly, because it's faster and easier. Slowly they drift: a validation that only runs over HTTP, a field only the local path returns, an auth check one of them skips.

This repo's next SDK refuses to have a second API.

A fetch that never leaves the process

const web = HttpRouter.toWebHandler(createEmbeddedRoutes(), { ... })

const fetch = (input, init) => web.handler(new Request(input, init))

const client = yield* Client.make({ baseUrl: "http://[name].local" })
  .pipe(Effect.provideService(FetchHttpClient.Fetch, fetch))
  1. Build the real routesThe embedded host builds the same server routes and turns them into a web handler, scoped so it's disposed when the host is.
  2. Swap only the transportThe generated client is given a fetch that calls the handler directly with a real Request. The base URL is a local hostname that never resolves on a network.
  3. Add only what's truly localThe embedded host extends the client with same-process capabilities, like registering application tools, instead of reimplementing the API.

Every embedded call goes through the same router and handlers as a remote one. The glossary even warns against calling it a "local implementation": it isn't one.

One source of truth, generated outward

Generated clients

Promise and Effect clients are generated from the public HTTP API. The agent guide says never to edit the generated folders by hand; change the API and regenerate.

A contract IR

A runtime-neutral compiled form of the API sits in between, so independent SDK generators can each choose their own value model and runtime.

One page shape

Pagination is a Page everywhere: items plus opaque previous and next cursor links for the same query in either direction.

Directed dependencies

"Client runtime code may depend on Schema and Protocol but never Core or Server." The client can't take a shortcut into server internals.

Steal this

If you ship an embedded mode, run it through your public API with an in-memory transport instead of calling internals. Generate the clients, forbid client imports of server code, and the two modes can't drift.

Use it in your code

A prompt for your coding agent

Run it from the root of your repo. It checks whether the idea fits before it changes anything.

Check whether this project's in-process or embedded mode goes through the same API as remote clients.

The idea: if you have both an HTTP API and a local, embedded or SDK mode, run the local mode through the real router and handlers with an in-memory transport (for example a fetch function that calls the handler directly) instead of calling internal functions. Generate clients from the API definition, and don't let client code import server internals.

1. Find every way code reaches the core: HTTP routes, SDKs, CLIs, embedded modes, test helpers. List where they bypass the HTTP layer and call internals directly.
2. For each bypass, note what could drift: validation, auth, error shapes, pagination, defaults.
3. Propose the smallest change that routes one bypass through the real handlers in memory.
4. If clients are generated, check that nobody edits them by hand. If there's a dependency rule such as "clients never import server code", suggest a check that enforces it.

Implement only the first step, with a test that the in-memory and HTTP paths return the same result.

Read the source

Where this comes from

OpenCode (anomalyco/opencode), the open-source coding agent (these lessons are from its upcoming v2). The files behind this lesson: