# 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.

Source: https://voxgig.com/howto/measure-entity-versus-endpoint-shaped-output

- Audience: api-producer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## 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](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) will walk it if you want
this automated. The naming conventions the parsing assumes come from [the Google API design
guide](https://cloud.google.com/apis/design/naming_convention), 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](https://learn.microsoft.com/en-us/openapi/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](https://openapi-generator.tech/docs/generators) | 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](https://www.stainless.com/docs) | 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](https://github.com/voxgig/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.

```js title="shape.mjs"
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.

```js title="shape.mjs"
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.

```bash
node demo.mjs
```

```text output
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.

```bash
node --test shape.test.mjs
```

```text output
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

- [Decide between generating and hand-writing a client](/howto/generate-or-hand-write-a-small-api-client)

- [Describe pagination so generators can follow it](/howto/describe-pagination-in-openapi-for-generators)

## Last verified

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

[^1]: The guide has moved and kept its rules. The address the page links redirects to
[AIP-190](https://google.aip.dev/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.

[^2]: The identifier is optional and the advice is soft. The
[OpenAPI Specification](https://spec.openapis.org/oas/v3.1.0) 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.

[^3]: Google's own vocabulary has room for such verbs, and a punctuation mark to announce them.
[AIP-136](https://google.aip.dev/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.