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
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Kiota | You want the URL structure visible, because your API is organized by path | A builder chain per call, which reads as the path rather than as the domain | Readers think in resources rather than in routes |
| OpenAPI Generator | Many languages, and conventions that follow the description closely | Names derived from operation ids, so a weak description gives a weak surface | The description’s operation ids were never designed |
| Stainless | You want resource-shaped output and are willing to configure it | A hosted service in the release path, and its own configuration model to learn | You need the generator to run entirely in your own pipeline |
| sdkgen | You want the same entity shape across several languages from one model | A model to maintain, and an opinion about entities your API may not share | Your 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.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
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
VerbNouninUpperCamelCase. It also asks that names used in APIs be in correct American English, and gives as its exampleslicenseinstead oflicenceandcolorinstead ofcolour. A guide that has to say which English it means has met the other one. ↩︎ Back to text -
The identifier is optional and the advice is soft. The OpenAPI Specification says an
operationIdMUST 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 handedgetMetersByIdis following the only rule that was written down, and so was the person who wrote it. ↩︎ Back to text -
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