npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brabander/nswag

v0.5.1

Published

`@brabander/nswag` (`type:adapter`, `scope:shared`) bevat de initiële NSwag-opzet voor het genereren van een TypeScript API-client uit een OpenAPI/Swagger-specificatie.

Readme

nswag

@brabander/nswag (type:adapter, scope:shared) bevat de initiële NSwag-opzet voor het genereren van een TypeScript API-client uit een OpenAPI/Swagger-specificatie.

Inhoud

  • nswag.json — de NSwag-configuratie: genereert een TypeScript-client met het Axios-template, DTO's als classes (niet interfaces), optionele parameters en ?-properties. Client-splitsing gebeurt per resource via operationGenerationMode: MultipleClientsFromOperationId (operationId's volgen de vorm Resource_Actie, bv. Products_GetAll). Gebruikt een clientBaseClass (ApiServiceBase) en een templateDirectory met custom Liquid-templates (zie hieronder) om duplicatie in de gegenereerde clients te verkleinen. De document-url en output zijn $(DocumentUrl)/$(Output)-variabelen (met fallback via defaultVariables naar de fixture hieronder) zodat andere apps dit ene config-bestand hergebruiken via nswag run nswag.json /variables:... in plaats van hun eigen nswag.json te dupliceren — zie "Gebruik door consumerende apps" hieronder.
  • swagger.json — een voorbeeld-OpenAPI 3.0-specificatie met 20 endpoints verdeeld over drie resources (Products, Customers, Orders), inclusief search/list (GET met query-parameters), GET by id, POST, PUT, PATCH en DELETE. Dient uitsluitend als fixture om de generatie/templates van dit package te testen — dit is niet de plek voor de spec van een echte applicatie, zie "Gebruik door consumerende apps" hieronder.
  • src/lib/api-service-base.ts — handgeschreven ApiServiceBase met httpGet/httpPost/httpPut/httpPatch/httpDelete, gebouwd op @brabander/http-client-axios. Gegenereerde clientclasses erven hiervan i.p.v. zelf axios-request/response-boilerplate te dupliceren. Bevat ook handleResult/toResult: één centrale plek die een httpX(...)-call, een verwachte succes-statuscode en optionele parseValue/parseError-functies omzet naar een Result<T, E | Error> (uit @brabander/result) — HTTP-foutstatussen worden naar de bijpassende Result-variant gemapt, en transportfouten (netwerk, timeout, abort) of een gooiende parser worden opgevangen en ook als Result.error(...) teruggegeven. De Promise die een gegenereerde methode teruggeeft reject dus nooit meer. Als er geen AxiosInstance wordt meegegeven aan de constructor, maakt ApiServiceBase er zelf één aan met default Content-Type/Accept: application/json-headers — zo hoeft geen enkele gegenereerde operation deze headers nog zelf te emitten (zie ook de kanttekening hieronder).
  • templates/ — custom Liquid-templates die NSwag's ingebouwde AxiosClient.liquid/Client.RequestUrl.liquid/File.Header.liquid overriden (via templateDirectory in nswag.json). Elke operation bouwt de request-URL/headers zoals voorheen, maar roept nu in één keer this.handleResult(this.httpX(...), successStatus, parseValue, parseError) aan op ApiServiceBase en geeft Promise<Result<T, E | Error>> terug — er wordt geen aparte processX-methode meer gegenereerd en er wordt niet meer gegooid bij een foutstatus. Omdat AxiosClient.liquid NSwag's ingebouwde Client.ProcessResponse.*-templates niet meer aanroept, worden die eenvoudigweg niet meer gerenderd (geen aparte override nodig). NSwag's eigen File.liquid/File.Utilities.liquid (niet overridden) genereren nog altijd een ongebruikte ApiException-class en throwException/isAxiosError-functies als eenmalige footer; dat is onschadelijke restcode (noUnusedLocals: false in tsconfig.lib.json tolereert dit al) en wordt niet apart weggewerkt. templates/Class.liquid overridet daarnaast NJsonSchema's ingebouwde DTO-template zodat gegenereerde classes BaseDto + @date/@dto/@array gebruiken — zie de aparte sectie hieronder. templates/Client.RequestUrl.liquid bouwt querystrings op via @brabander/http-client's QueryStringBuilder (addString/addNumber/addBoolean/addDate, hun ...When-varianten, addStringArray/addNumberArray/addDateArray, addObjectArray) i.p.v. inline string-concatenatie per parameter — zie de kanttekening hieronder over het encoding-verschil met de oude aanpak.
  • src/lib/dto/base-dto.ts + src/lib/dto/decorators/ — handgeschreven BaseDto<T> en de @date/@dto/@array-decorators die DTO-classes gebruiken om duplicatie in constructor/init/fromJS-boilerplate te verkleinen. Zie de aparte sectie hieronder.
  • src/generated/ — waar nx run nswag:generate (zonder /variables:-overrides) de fixture-client (ProductsClient, CustomersClient, OrdersClient + DTO-classes) uit swagger.json neerzet. Dit is een gitignored build-artefact, niet gecommit en niet geëxporteerd via src/index.ts — niets in dit package of elders in de workspace gebruikt deze fixture-classes. Het dient uitsluitend als lokale smoke-test: draai nx run nswag:generate na een wijziging aan swagger.json of de templates om te controleren dat de generatie nog slaagt en er valide TypeScript uitkomt.

Kanttekeningen bij de custom templates

  • ApiServiceBase's methodes heten bewust httpGet/httpPost/httpPut/httpPatch/httpDelete i.p.v. get/post/put/patch/delete: OpenAPI-operations heten vaak letterlijk zo (bv. een Delete- of Patch-operation), en zo'n gegenereerde methode zou een gelijknamige base-class-methode stilzwijgend overriden i.p.v. hem aan te roepen — TypeScript ving dit gelukkig af als compile-error (TS2416) tijdens het testen van deze templates, maar de http-prefix voorkomt de botsing structureel.
  • Alleen JSON-request-bodies worden ondersteund (operation.HasContent) — form-data/multipart/urlencoded bodies en file upload/download (IsFile) worden niet door deze fork afgehandeld; swagger.json heeft hier vandaag geen operations van.
  • UseTransformOptionsMethod/UseTransformResultMethod-hooks worden niet ondersteund door deze fork; geef in plaats daarvan een geconfigureerde AxiosInstance mee aan de constructor voor interceptor-achtige aanpassingen. Standaard Content-Type/Accept: application/json-headers zitten sinds kort op die (automatisch aangemaakte) AxiosInstance i.p.v. per-operation in AxiosClient.liquid — geef zelf een AxiosInstance mee als je andere default-headers nodig hebt; die is dan zelf verantwoordelijk voor headers (geen automatische merge).
  • Querystrings worden opgebouwd via @brabander/http-client's QueryStringBuilder, die intern URLSearchParams gebruikt. Dat encodeert een spatie als +, terwijl de oude inline encodeURIComponent-aanpak een spatie als %20 encodeerde — een wire-format-verandering. Nog niet geverifieerd tegen een echte backend; controleer bij het aansluiten van de echte spec of + in een querystring als spatie geïnterpreteerd wordt (bv. ASP.NET Core model binding), en pas zo nodig de builder aan om spaties expliciet als %20 te coderen.
  • Gebruik de extensionCode-instelling niet om de ApiServiceBase-import toe te voegen: in NSwag 14.7.1 lekt daarbij een fragment van een absoluut pad aan het eind van het gegenereerde bestand (geïsoleerd gereproduceerd met alléén extensionCode gezet, los van templateDirectory/clientBaseClass/useAbortSignal). De import wordt in plaats daarvan toegevoegd via een override van File.Header.liquid (die nu ook de Result-import bevat).
  • Documenteer wijzigingen aan de templates hier in plaats van in de .liquid-bestanden zelf — een eerdere {% comment %}-poging werd voor de zekerheid verwijderd tijdens het isoleren van de extensionCode-bug hierboven en niet teruggezet.
  • Error-response pattern (Result i.p.v. throw): de nieuwe templatelogica gaat ervan uit dat elke operation hooguit één success-response en hooguit één getypeerde non-success-response heeft (vandaag altijd waar: elke operation in swagger.json heeft precies één 2xx-status, en foutresponses zijn altijd 400 of 404, getypeerd als ProblemDetails). Bij meerdere getypeerde succes- of foutresponses per operation pakt de template alleen de eerste; dat is een bewuste vereenvoudiging, geen bug — pas de template aan als de spec dit ooit nodig heeft.
  • Bewuste gedragswijziging: een HTTP-status die niet in de spec van die specifieke operation voorkomt, maar wel op de lijst 400/401/403/404/409 staat, wordt nu een getypeerd Result-foutobject i.p.v. een throw. Dat is kleiner/eenvoudiger dan het oude exacte-match-of-throw-gedrag, maar wel een contractwijziging waar consumers van de client rekening mee moeten houden.
  • Optionele, niet-nullable query-parameters gooien niet meer bij null: NSwag's standaardgedrag was om voor een optionele query-parameter die volgens de spec niet nullable is (bv. search in getAll) toch een if (x === null) throw ...-guard te genereren vóór de qs_.addString(...)-call. Overbodig, want QueryStringBuilder.add() (aangeroepen door addString/addNumber/addBoolean/addDate) skipt null/undefined sowieso al stilzwijgend via isDefined (@brabander/core). Client.RequestUrl.liquid gebruikt voor zulke optionele parameters nu i.p.v. de throw+plain-add de ...When-variant met isDefined als predicate (bv. qs_.addStringWhen(isDefined, "search", search)), en de bijbehorende parameter-types in AxiosClient.liquid zijn aangepast van T | undefined naar Nullable<T> zodat de signature het (al bestaande) runtime-gedrag eerlijk weergeeft. QueryStringBuilder's addStringWhen/addNumberWhen/addBooleanWhen/addDateWhen namen daarvoor een plain boolean als eerste argument; dat is een predicate: (value) => boolean-callback geworden, zodat isDefined er direct in past.

BaseDto en mapping-decorators

Elke NSwag-gegenereerde DTO-class herhaalde vroeger dezelfde boilerplate: een constructor, een init(_data) die per property handmatig new Date(...) of X.fromJS(item) deed, een static fromJS, en een toJSON. src/lib/dto/base-dto.ts en src/lib/dto/decorators/ vervangen dat door een BaseDto-basisclass plus drie property-decorators die metadata registreren, en één generieke init() op BaseDto die die metadata gebruikt om de ruwe constructor-data te mappen:

export class Order extends BaseDto {
  constructor(data?: IOrder) {
    super(data);
  }

  id!: string;
  customerId!: string;
  @date() createdAt?: Date;
  @array(() => OrderItem) items!: OrderItem[];
}
export type IOrder = PublicProperties<Omit<Order, 'init'>>;
  • Gewone properties (id, customerId) hebben geen decorator en geen declare nodig — gewoon een normale !/?-veld-declaratie.
  • @date() — maakt er bij constructie daadwerkelijk een new Date(...) van.
  • @dto(() => Ctor) — maakt er daadwerkelijk een new Ctor(...) van (voor een geneste DTO-class).
  • @array(() => Ctor) — mapt elk element van een array via mapModel naar new Ctor(item) (of laat een element dat al een Ctor-instance is ongemoeid). Alléén bedoeld voor arrays van geneste DTO-classes, niet voor Date[]/primitieven — zie "Bekende beperkingen" hieronder.
  • static fromJS(data) — blijft bestaan (return new ClassName(data)) omdat de (ongewijzigde) AxiosClient.liquid/Client.RequestUrl.liquid-templates dit aanroepen om response-data te parsen.
  • I{ClassName} wordt niet meer apart door NJsonSchema gegenereerd, maar afgeleid van de class zelf via PublicProperties<Omit<{ClassName}, 'init'>> (een utility type uit src/lib/dto/metadata.ts) — de publieke properties van het model zíjn het interface-type, zonder aparte generatie die uit de pas kan lopen.
  • Geen toJSON meer nodig: properties zijn gewone (enumerable) class fields, dus JSON.stringify(instance) serialiseert ze al correct — geneste Dto-instances en Date (die zelf al een ingebouwde toJSON heeft) recursief inbegrepen.

Waarom dit werkt: experimentalDecorators + useDefineForClassFields: false

@date/@dto/@array zijn legacy TypeScript property-decorators ((target, propertyKey) => void), niet de nieuwe TC39 stage-3 decorators. Ze schrijven metadata (welke properties getransformeerd moeten worden, en hoe) op de class-prototype onder een niet-geëxporteerde Symbol, gemerged per class zodat overerving automatisch werkt. BaseDto.init(data) leest die metadata terug en past de transformatie generiek toe.

Dit vereist packages/nswag/tsconfig.lib.json/tsconfig.spec.json: "experimentalDecorators": true (voor de decorator-syntax zelf) én "useDefineForClassFields": false. Dat laatste is essentieel en niet optioneel: het root-tsconfig.base.json heeft target: "es2022", en bij useDefineForClassFields op de (impliciete) default true initialiseert élk class-field van een subclass zich opnieuw naar undefined zodra super(data) terugkeert — dat overschrijft alles wat BaseDto's init() (aangeroepen vanuit de BaseDto-constructor, dus vóór die subclass-veld-initialisatie) net heeft toegekend. Empirisch geverifieerd tijdens de implementatie: zonder useDefineForClassFields: false werden zelfs de gedecoreerde properties stilzwijgend undefined.

Bekende beperkingen:

  • @dto/@array verwachten een thunk (() => Ctor), niet de class-referentie direct — zo kan een Dto-class verwijzen naar een andere Dto-class die pas later in hetzelfde gegenereerde bestand gedeclareerd wordt, zonder een temporal-dead-zone-fout (de thunk wordt pas op constructie-tijd aangeroepen, niet bij class-declaratie).
  • @array(Ctor) roept intern new Ctor() (zonder argumenten) aan en checkt daarna instanceof BaseDto om te weten of .init(...) aangeroepen moet worden — dit werkt dus alléén voor arrays van andere BaseDto-subclasses, niet voor Date[] of arrays van primitieven (die hebben ook geen decorator nodig, ze worden gewoon 1-op-1 gekopieerd).
  • Vitest/Vite in deze workspace (rolldown + oxc) kan legacy decorator-syntax nog niet parsen — zelfs class Foo { @bar() x; } los van alle eigen logica geeft een SyntaxError. tsc (de daadwerkelijke compiler, gebruikt voor nx build) compileert dezelfde code wél probleemloos. vitest.config.mts van dit package bevat daarom een lokale Vite-plugin die dit package's .ts-bestanden eerst via @swc/core (al een workspace-devDependency) transformeert, met dezelfde experimentalDecorators/useDefineForClassFields-instellingen als de tsconfig's — zie de comment daar. Zodra oxc/rolldown dit zelf ondersteunt, kan die plugin weer weg.

templates/Class.liquid — scope van de override

Deze template overridet NJsonSchema's ingebouwde DTO-generatie (niet NSwag's eigen client-templates) zodat platte/datum-/array-/geneste-object-properties bovenstaande vorm krijgen. Alleen deze gevallen worden gemodelleerd; voor elke class die overerving, discriminators, dictionaries of abstracte classes gebruikt (of generateCloneMethod aanstaat), valt de template terug op NJsonSchema's originele, volledige generatie — als letterlijk ingesloten kopie van de upstream Class.liquid (NJsonSchema's eigen {% template %}-tag blijkt alleen te werken als het hele bestand vervangt, niet genest in een {% if %}, dus een echte kopie was de enige werkende optie). Een indexer-property ([key: string]: any) triggert deze fallback bewust niet — zie de code-comment in het template voor waarom. De huidige swagger.json-fixture gebruikt geen van de fallback-triggerende features, dus die tak is verified-to-parse maar niet in de praktijk doorlopen; controleer dit lokaal (regenereren + inspecteren) voordat een consumerende app een spec aanlevert die er wel gebruik van maakt.

  • nswag.json heeft "convertConstructorInterfaceData": true nodig — zonder die instelling rapporteert NJsonSchema voor elke property (ook geneste objecten/arrays) SupportsConstructorConversion: false, waardoor het template nooit @dto/@array zou toevoegen.

Dit alles is echt gevalideerd in een sandbox met .NET 8 SDK (via Ubuntu's eigen apt-repository, niet via het door het netwerkbeleid geblokkeerde dotnet.microsoft.com): nx run nswag:generate → nx run nswag:build → nx test nswag liepen allen succesvol door, en een los smoke-testje bevestigde dat new Product(data).createdAt en new Order(data).items[0] daadwerkelijk instanceof Date/instanceof OrderItem zijn.

Client genereren

Vereist een lokaal geïnstalleerde .NET SDK (NSwag draait als .NET-tool onder water, ook via de npm-wrapper). Installeer eerst de dependencies (@brabander/result is een nieuwe dependency van dit package) voordat je regenereert:

npm ci
nx run nswag:generate
# of rechtstreeks:
npx nswag run nswag.json

Gebruik door consumerende apps

@brabander/nswag is de gedeelde, herbruikbare basis: één nswag.json (deze, in dit package) met de Liquid-templates in templates/ en de handgeschreven runtime (ApiServiceBase, BaseDto, de @date/@dto/@array-decorators, PublicProperties). Een applicatie die een eigen backend-spec heeft, roept dit ene config-bestand rechtstreeks aan en overschrijft alleen de paden die per app verschillen — er komt geen eigen nswag.json per app bij. De gegenereerde client hoort wél in de app zelf te wonen, niet in dit package.

Dit werkt via NSwag's eigen variabelen-mechanisme: documentGenerator.fromDocument.url en codeGenerators.openApiToTypeScriptClient.output zijn in dit nswag.json $(DocumentUrl)/$(Output)-placeholders, met fallback-waarden in "defaultVariables" die dit package's eigen swagger.json-fixture genereren. Een consumerende app overschrijft ze via nswag run <pad-naar-dit-nswag.json> /variables:DocumentUrl=...,Output=....

Belangrijk: alle relatieve paden in dit nswag.json (inclusief $(DocumentUrl)/$(Output)/templateDirectory) worden door NSwag opgelost relatief aan de map van dit config-bestand (packages/nswag), ongeacht vanuit welke working directory de CLI wordt aangeroepen — dus ook een override-waarde die vanuit een andere app komt, wordt geschreven als een pad relatief aan packages/nswag. templateDirectory staat vast op "templates" (relatief aan dit package) en hoeft nooit overschreven te worden — templates leven altijd hier, ongeacht welke app genereert.

apps/school is het referentievoorbeeld:

  • apps/school/src/app/client/openapi.yaml — de OpenAPI-spec van de school-backend.
  • apps/school/package.json — "generate": "nswag run ../../packages/nswag/nswag.json /variables:DocumentUrl=../../apps/school/src/app/client/openapi.yaml,Output=../../apps/school/src/app/client/generated/api-client.ts" (beide paden relatief aan packages/nswag, zie hierboven) — zo werkt nx run school:generate. Heeft ook @brabander/nswag (voor ApiServiceBase/BaseDto/decorators), axios (zelfde ^1.18.1-range als dit package, om npm-hoisting van axios-types consistent te houden) en nswag (de CLI) als dependency.
  • apps/school/tsconfig.app.json/tsconfig.spec.json — hebben dezelfde experimentalDecorators: true/useDefineForClassFields: false nodig als dit package (zie "Waarom dit werkt" hierboven).

Belangrijk voor herbruikbaarheid: templates/File.Header.liquid importeert ApiServiceBase/BaseDto/de decorators/PublicProperties via '@brabander/nswag' (pakket-naam), niet via relatieve paden — zo werkt dezelfde templateDirectory voor elke consumerende app, ongeacht waar de gegenereerde output terechtkomt.

Een nieuwe app die dit patroon wil volgen: voeg alleen een "generate"-script toe dat packages/nswag/nswag.json aanroept met eigen DocumentUrl/Output-variabelen (paden relatief aan packages/nswag), en zorg dat @brabander/nswag + axios (zelfde range) als dependency zijn opgenomen. Geen nieuw config-bestand nodig.

This library was generated with Nx.

Building

Run nx build nswag to build the library.

Running unit tests

Run nx test nswag to execute the unit tests via Vitest.