How-to › Ship an SDK

How to tell an entity-shaped SDK from an endpoint-shaped one#

Count the nouns, the operations per noun, and how many names start with a verb, so a claim about SDK shape is a measurement rather than a preference.

Audience
API producer
Level
intermediate
Topic
Choose a generator and generate an SDK
Languages
TypeScript and JavaScript
Verified

Two generators produce clients for the same API and the team argues about which reads better. One side says the output is a wrapper around URLs, the other says the alternative hides what the API actually does. Both arguments are about taste, the decision gets made by whoever speaks last, and the client ships for five years.

What you get

You will end up with four numbers per client: how many operations, how many nouns, how many operations each noun carries, and what share of the names start with a verb. This is for you if you are comparing generators and want the discussion anchored to something.

Short answer

List the client’s exported operations and parse each name into a verb and a noun. Count the distinct nouns, the operations per noun, and the share of names that start with the verb. A surface where most names start with a verb is endpoint shaped, and a vocabulary larger than the number of resources is a naming inconsistency rather than a style.

You will need

Node 22 or later, and the exported symbol list from one or more generated clients. Any generator can produce it: for TypeScript, the declaration file lists the surface, and the TypeScript compiler API will walk it if you want this automated. The naming conventions the parsing assumes come from the Google API design guide, which most generators follow loosely.1

Voxgig maintains sdkgen. This page measures its output the same way it measures Kiota, OpenAPI Generator and Stainless, on a surface all four can produce.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
KiotaYou want the URL structure visible, because your API is organized by pathA builder chain per call, which reads as the path rather than as the domainReaders think in resources rather than in routes
OpenAPI GeneratorMany languages, and conventions that follow the description closelyNames derived from operation ids, so a weak description gives a weak surfaceThe description’s operation ids were never designed
StainlessYou want resource-shaped output and are willing to configure itA hosted service in the release path, and its own configuration model to learnYou need the generator to run entirely in your own pipeline
sdkgenYou want the same entity shape across several languages from one modelA model to maintain, and an opinion about entities your API may not shareYour API does not divide into entities

The numbers do not say which shape is better, but they are still worth having. What they do say is whether a generator produced what you expected, and whether the API underneath has a consistent vocabulary at all. Both are questions people argue about without evidence. A surface with more nouns than resources is telling you about the description rather than about the generator.

Shape follows the description more than the marketing of either tool suggests. An OpenAPI document with tags and consistent operation ids gives most generators something resource-shaped to work with, and one with getMetersById for an operation id gives all of them the same problem.2

Parse the name, not the documentation

One function turns a symbol into a verb and a noun.

function parse(symbol) {
  if (symbol.includes('.')) {
    const parts = symbol.split('.')
    return { noun: parts[0], depth: parts.length - 1, verbFirst: VERBS.includes(parts[0]) }
  }
  const words = symbol.replace(/([A-Z])/g, ' $1').trim().toLowerCase().split(/\s+/)
  const verbFirst = VERBS.includes(words[0])
  const noun = (verbFirst ? words[1] : words[0]) ?? symbol
  return { noun: singular(noun), depth: 1, verbFirst }
}

Measure the exported surface rather than the internal one. What a reader types is the thing under discussion, and a client that is resource-shaped internally and flat at the boundary reads as flat.

Count nesting separately from the rest. Sub-resources are where flat naming gets long, because the whole path has to be spelled out in the name. A reader looking for readings on a meter then has to guess the word order. Nesting is where the two shapes diverge most, so a client with none of it is usually flat however its top-level names read.

Read the vocabulary, not only the counts

The list of nouns is the part that finds real problems.

export function vocabulary(symbols) {
  return [...new Set(symbols.map((s) => parse(s).noun))].sort()
}

A vocabulary longer than your resource list means the same thing is named two ways. That is a finding about the API, and it survives whichever generator you pick.

Check it worked

Measure three clients over the same thirteen operations.

node demo.mjs
flat-client
  operations         13
  nouns              3
  operationsPerNoun  4.33
  verbFirstShare     1
  nested             0
  shape              endpoint-shaped
  vocabulary         account, invoice, meter
entity-client
  operations         13
  nouns              3
  operationsPerNoun  4.33
  verbFirstShare     0
  nested             2
  shape              entity-shaped
  vocabulary         account, invoice, meter
mixed-client
  operations         13
  nouns              5
  operationsPerNoun  2.6
  verbFirstShare     0.31
  nested             0
  shape              entity-shaped
  vocabulary         account, invoices, meter, meters, void

The first two cover the same operations and the same three nouns. Neither is more capable; they are addressed differently, and the measurement says so rather than ranking them. Which one your readers prefer is a question for your readers, and now you can put two numbers in front of them instead of two opinions.

The third client is the finding. Five nouns for three resources, because meter and meters are both in the surface, and void was read as a noun because voidInvoice puts an unusual verb first. That is a naming problem in the API, and it will follow you to any generator you choose.

node --test shape.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 147.546757

When it goes wrong

Every name parses as a noun. The verb list does not cover your conventions. Extend it with the verbs your API actually uses, and re-read the vocabulary afterwards. A domain verb such as void, settle or provision belongs in the list as much as get does.3

The shape label disagrees with what you see. A threshold of half is arbitrary, and a client at 0.45 is mixed rather than entity shaped. Report the share and treat the label as a summary.

Singular and plural both appear. The stemming is crude, which is the point: it shows you the inconsistency rather than smoothing it away. A cleverer word normalizer would report three nouns and hide the problem.

Two generators score identically and feel different. The difference is in types, errors, or pagination rather than in naming. Measure those separately instead of stretching this metric. A metric asked to cover everything ends up measuring nothing anyone recognizes.

When not to do this

Do not use these numbers to pick a generator on their own. Shape is one axis, and retries, types, error handling and language coverage decide more for most teams.

Do not treat entity shape as a goal for an API that has no entities. A reporting API with twelve unrelated aggregations has nothing to group under, and forcing nouns onto it invents a domain model nobody asked for.

Do not expect sdkgen or any generator to fix a vocabulary problem. Generators name things after what the description calls them, and a description with two words for one resource produces a client with two words for one resource. Fix the description first, then regenerate, then measure again.

Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

Footnotes

  1. The guide has moved and kept its rules. The address the page links redirects to AIP-190, one of Google’s API Improvement Proposals, which asks for method names of the form VerbNoun in UpperCamelCase. It also asks that names used in APIs be in correct American English, and gives as its examples license instead of licence and color instead of colour. A guide that has to say which English it means has met the other one. ↩︎ Back to text

  2. The identifier is optional and the advice is soft. The OpenAPI Specification says an operationId MUST be unique among all operations and is case-sensitive. Since tools MAY use it to identify an operation, it adds, it is RECOMMENDED to follow common programming naming conventions. Which conventions is not said. A generator handed getMetersById is following the only rule that was written down, and so was the person who wrote it. ↩︎ Back to text

  3. Google’s own vocabulary has room for such verbs, and a punctuation mark to announce them. AIP-136 covers custom methods, which are everything the five standard methods do not. It requires the HTTP URI to use a colon and then the custom verb, as in :archive, and the verb in the URI to match the verb in the RPC name. The proposal has a section headed Disallowing prepositions. A verb that needed a colon to be admitted is a verb the resource model had no noun for. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.