@astrotech-sh/nuvicms-api-sdk
v0.3.1
Published
TypeScript HTTP client (ports-and-adapters) for the NuviCMS public API.
Readme
nuvicms-api-sdk
TypeScript HTTP client for the NuviCMS public API, built with a ports-and-adapters (hexagonal) architecture — zero runtime dependencies, DOM-free core, fully injectable I/O.
Extracted from vape-shop-022-site (originally nuvicms-client) for reuse across multiple client projects.
Status
Published to the public npm registry as @astrotech-sh/nuvicms-api-sdk version 0.1.0.
Installation
The SDK is published to the public npm registry — no custom registry configuration needed:
npm install @astrotech-sh/nuvicms-api-sdkContent/CMS-shape types (pages, products, posts, etc.) are available from the ./content subpath and are type-only:
import type { CmsPage, CmsProduct } from "@astrotech-sh/nuvicms-api-sdk/content";Quick Start
import { createNuviCmsClient } from "@astrotech-sh/nuvicms-api-sdk";
// Create a client with configuration
const client = createNuviCmsClient({
apiBaseUrl: "https://api.nuvicms.com.br/api/public",
siteKey: "your-site-key",
});
// Read store configuration
const config = await client.store.fetchConfig();
if (!config) {
console.error("Store not configured");
return;
}
// Validate a coupon
const validationResult = await client.coupons.validate("DISCOUNT10");
if (validationResult.valid) {
console.log(`Discount: ${validationResult.coupon?.discountValue}`);
}
// Create an order
try {
const order = await client.orders.create({
customerRef: "customer-123",
items: [
{ productId: "prod-1", quantity: 2 },
],
delivery: { kind: "pickup" },
payment: { method: "pix" },
});
console.log(`Order created: ${order.id}`);
} catch (error) {
if (error instanceof NuviCmsConfigError) {
console.error("Client not configured with apiBaseUrl");
}
}Public Surface
| Export | Type | Purpose | Documentation |
|--------|------|---------|----------------|
| createNuviCmsClient(config, ports?) | Factory | Creates a client instance | getting-started |
| Configuration | | | |
| NuviCmsConfig | Type | Client config shape | configuration |
| NormalizedNuviCmsConfig | Type | Internal normalized shape | configuration |
| Errors | | | |
| NuviCmsError | Class | Base error type | errors |
| NuviCmsHttpError | Class | Non-2xx HTTP response | errors |
| NuviCmsEnvelopeError | Class | Malformed API envelope | errors |
| NuviCmsTransportError | Class | Network/CORS/abort error | errors |
| NuviCmsParseError | Class | JSON parse error | errors |
| NuviCmsConfigError | Class | Missing apiBaseUrl | errors |
| Ports | | | |
| NuviCmsPorts | Type | Complete port set | ports |
| HttpPort, StoragePort, LoggerPort, etc. | Types | Individual port contracts | ports |
| AbortSignalLike | Type | Cancellation signal | ports |
| Store Configuration | | | |
| client.store.fetchConfig() | Method | Fetch store/catalog config | store-config |
| client.store.trackFilterEvents() | Method | Send filter/search telemetry | filter-events |
| StoreConfig, StoreConfigPix | Types | Store shape | configuration |
| Coupons | | | |
| client.coupons.validate(code, context?) | Method | Validate a coupon code | coupons |
| CouponValidationResult, ValidatedCoupon | Types | Result types | coupons |
| Customers | | | |
| client.customers.getByClientRef(ref) | Method | Look up customer by ref | customers |
| client.customers.upsert(payload) | Method | Create or update customer | customers |
| CustomerData, CustomerAddress | Types | Customer shape | customers |
| Orders | | | |
| client.orders.create(request) | Method | Create an order | orders |
| OrderRequest, OrderResponse | Types | Order shape | orders |
| Forms | | | |
| client.forms.getBySlug(slug) | Method | Fetch a form by slug | forms |
| client.forms.submit(slug, values) | Method | Submit form values | forms |
| CmsForm, CmsFormField, CmsFormValues | Types | Form shape | forms |
| validateFieldValue, validateFormValues | Functions | Client-side validation | forms |
| FormValidationCode, FormValidationResult | Types | Validation result | forms |
| Pages | | | |
| client.pages.getBySlug(slug, options?) | Method | Fetch a page by slug | pages |
| Page, PageSection, PageModule, PageModuleData | Types | Page shape | pages |
| PageSeo, PageFaqModuleData, PageFormModuleData, ... | Types | Module and SEO types (15+ types) | pages |
| SEO & Public Content | | | |
| client.sitemap.get() | Method | Fetch sitemap entries | sitemap |
| client.robots.get() | Method | Fetch robots.txt directives | robots |
| client.website.get() | Method | Fetch website configuration | website |
| SitemapEntry, Robots, RobotsUserAgentGroup | Types | Sitemap and robots shape | sitemap, robots |
| WebsiteInfo, WebsiteAddress, WebsiteIntegrations, ... | Types | Website info structure (20+ types) | website |
| Pricing | | | |
| calculateDiscounts(cart, rules) | Function | Compute order discounts | pricing |
| resolveProductScopeIds(cart) | Function | Extract product IDs for coupon scope | pricing |
| DiscountCartItem, DiscountResult | Types | Discount shapes | pricing |
| Blog & Posts | | | |
| client.posts.list(params?) | Method | Paginated post list, optionally filtered by category | posts |
| client.posts.getBySlug(slug, options?) | Method | Fetch a single post by slug | posts |
| client.posts.submitComment(slug, input, options?) | Method | Submit a comment on a post | posts |
| client.blog.getFeedInventory(options?) | Method | Fetch the blog's RSS/feed inventory | blog |
| Post, GetPostsParams, SubmitPostCommentInput | Types | Post shape and inputs | posts |
| Catalog (Products) | | | |
| client.products.list(params) | Method | Paginated product card list | products |
| client.products.getBySlug(slug, options?) | Method | Fetch a single product by slug | products |
| client.products.listReviews(slug, params) | Method | Paginated product review list | products |
| client.products.submitReview(productId, payload, options?) | Method | Submit a product review | products |
| client.productCategories.list(options?) | Method | Fetch all product categories | product-categories |
| Product, ProductCard, ProductReview, ProductCategory | Types | Catalog shapes | products, product-categories |
| Galleries | | | |
| client.galleries.photos() | Method | Fetch all photo galleries | galleries |
| client.galleries.videos() | Method | Fetch all video galleries | galleries |
| PhotoGallery, GalleryPhoto, VideoGallery, GalleryVideo | Types | Gallery shapes | galleries |
| Institutional | | | |
| client.institutional.faq() | Method | Fetch all FAQ items | institutional |
| client.institutional.team() | Method | Fetch all team members | institutional |
| client.institutional.testimonials() | Method | Fetch all testimonials | institutional |
| FaqItem, TeamMember, Testimonial | Types | Institutional content shapes | institutional |
| Section Catalog | | | |
| client.sectionCatalog.list() | Method | Fetch the available page-section descriptors | section-catalog |
| SectionCatalogEntry | Type | Section descriptor shape | section-catalog |
| Events | | | |
| client.events.trackCta(event, idempotencyKey?) | Method | Track a CTA click/impression | events |
| client.events.trackPost(event, idempotencyKey?) | Method | Track a post interaction | events |
| CtaEvent, PostEvent | Types | Event shapes | events |
| Payments | | | |
| client.payments.getPublicKey(websiteExternalId) | Method | Fetch the payment provider's publishable key | payments |
| PaymentsPublicKey | Type | Public key shape | payments |
| Customers (extended) | | | |
| client.customers.getByExternalId(externalId) | Method | Look up customer by external ID | customers |
| Post Categories | | | |
| buildPostCategoryTree(categories) | Function | Organize flat categories into a tree | content-types |
| CmsPostCategory, CmsPostCategoryNode | Types | Category tree shapes | content-types |
| Browser Utilities | | | |
| cookieClientRefPort | Object | Browser session cookie adapter | ports |
| ensureAnonCookie() | Function | Create anon session if needed | ports |
Type-only entry point: ./content exports only types and has no runtime code. Safe to import in type-only contexts and unbundled in tree-shaking.
Resources and Endpoints
| Facade Method | HTTP Method + Path | Retried | Requires Config | Documentation |
|---------------|-------------------|---------|-----------------|---------------|
| store.fetchConfig() | GET /store-config | Yes | No | store-config |
| store.trackFilterEvents(events) | POST /filter-events | No | No | filter-events |
| coupons.validate(code, context?) | GET /coupons/:code/validate | Yes | No | coupons |
| customers.getByClientRef(ref) | GET /customers/:ref | Yes | No | customers |
| customers.upsert(payload) | POST /customers | No | Yes | customers |
| orders.create(request) | POST /orders | No | Yes | orders |
| forms.getBySlug(slug) | GET /forms/:slug | Yes | No | forms |
| forms.submit(slug, values) | POST /forms/:slug/submit | No | Yes | forms |
| pages.getBySlug(slug, options?) | GET /pages/:slug | Yes | No | pages |
| sitemap.get() | GET /sitemap | Yes | No | sitemap |
| robots.get() | GET /robots | Yes | No | robots |
| website.get() | GET /website | Yes | No | website |
| posts.list(params?) | GET /posts | Yes | No | posts |
| posts.getBySlug(slug, options?) | GET /posts/:slug | Yes | No | posts |
| posts.submitComment(slug, input, options?) | POST /posts/:slug/comments | No | Yes | posts |
| blog.getFeedInventory(options?) | GET /blog/feed-inventory | Yes | No | blog |
| products.list(params) | GET /products | Yes | No | products |
| products.getBySlug(slug, options?) | GET /products/:slug | Yes | No | products |
| products.listReviews(slug, params) | GET /products/:slug/reviews | Yes | No | products |
| products.submitReview(productId, payload, options?) | POST /products/:productId/reviews | No | Yes | products |
| productCategories.list(options?) | GET /product-categories | Yes | No | product-categories |
| galleries.photos() | GET /galleries/photos | Yes | No | galleries |
| galleries.videos() | GET /galleries/videos | Yes | No | galleries |
| institutional.faq() | GET /faq | Yes | No | institutional |
| institutional.team() | GET /team | Yes | No | institutional |
| institutional.testimonials() | GET /testimonials | Yes | No | institutional |
| sectionCatalog.list() | GET /section-catalog | Yes | No | section-catalog |
| events.trackCta(event, idempotencyKey?) | POST /cta-events | No | Yes | events |
| events.trackPost(event, idempotencyKey?) | POST /post-events | No | Yes | events |
| payments.getPublicKey(websiteExternalId) | GET /websites/:id/payments/public-key | Yes | No | payments |
| customers.getByExternalId(externalId) | GET /customers/:externalId | Yes | No | customers |
Retry policy: Only idempotent GETs are retried on transient failures. POST (state-changing) and telemetry calls are never retried. Requires Config: Mutations (orders.create, customers.upsert, forms.submit) throw NuviCmsConfigError if apiBaseUrl is missing; reads silently return null or skip the call. SEO endpoints (sitemap.get(), robots.get(), website.get()) are best-effort and return empty/null on failure without throwing.
Error Handling
All errors inherit from NuviCmsError and can be caught with instanceof:
import {
NuviCmsError,
NuviCmsHttpError,
NuviCmsConfigError,
} from "@astrotech-sh/nuvicms-api-sdk";
try {
const order = await client.orders.create(request);
} catch (error) {
if (error instanceof NuviCmsConfigError) {
// Client not configured; silent for reads, loud for mutations
console.error("Orders require apiBaseUrl");
} else if (error instanceof NuviCmsHttpError) {
// Non-2xx response
console.error(`API error ${error.status}:`, error.body);
} else if (error instanceof NuviCmsError) {
// Other SDK error (transport, parse, envelope)
console.error("SDK error:", error.message);
}
}See Error Handling for the complete hierarchy and contract.
Testing with Fake Ports
Inject fake I/O adapters in tests:
import { createNuviCmsClient } from "@astrotech-sh/nuvicms-api-sdk";
const fakeHttp = {
async send(request) {
if (request.path.includes("/store-config")) {
return {
status: 200,
body: JSON.stringify({
status: "success",
data: { currency: "BRL", methods: ["pix"] },
}),
};
}
return { status: 404, body: "Not found" };
},
};
const client = createNuviCmsClient(
{ apiBaseUrl: "https://api.test", siteKey: "test-key" },
{ http: fakeHttp } // Override the HTTP port
);
const config = await client.store.fetchConfig();
// config is { currency: "BRL", methods: ["pix"] }See Ports for the full port interface and Testing for browser vs. Node patterns.
Architecture
createNuviCmsClient(config, ports)
|
+-- client.ts (facade)
|
+-- resources/*.ts (store, coupons, customers, orders, forms)
|
+-- transport/request.ts:sendRequest() [single HTTP call site]
|
+-- ports/http.ts (injected; default: adapters/browser.ts)
+-- mappers/*.ts (pure raw -> domain translation)The client is shaped as a facade over per-resource modules. Every network call funnels through a single HTTP port, which by default uses fetch. Config is resolved once at construction and never re-read. Every side-effecting capability (fetch, sessionStorage, document, crypto, cookies) lives in adapters/browser.ts — the only place ambient DOM/runtime globals are allowed. Tests or non-browser contexts inject their own I/O ports.
The read/telemetry asymmetry is intentional: store.fetchConfig() and trackFilterEvents() silently no-op and return { kind: "skipped" } if apiBaseUrl is missing; mutations (orders.create, customers.upsert, forms.submit) throw NuviCmsConfigError instead. A dropped read is recoverable; a dropped order or customer record is not.
Development
Install dependencies, build, and test:
npm install
npm run build # TypeScript compilation (tsconfig.build.json)
npm test # VitestArchitecture Invariants
Three invariants are enforced by boundary tests (src/tests/boundary.test.ts and src/tests/dom-free.node.test.ts):
- No outbound imports: No file under
src/imports outsidesrc/. - No ambient globals outside adapters: No
fetch,sessionStorage,document,console,crypto, or other DOM/Node globals outsidesrc/adapters/and test files. - Zero runtime dependencies: The SDK has no production npm dependencies; only dev dependencies (TypeScript, Vitest).
Documentation
- Getting Started — Install, configure, and walk through a real flow
- Architecture — Hexagonal design, layers, and invariants
- API Reference — Per-resource endpoint documentation
- Configuration Reference — Config options and normalization
- Error Reference — Error hierarchy and handling
- Ports Reference — I/O port contracts and browser adapters
- LLM Context — Dense, self-contained reference for LLM/agent ingestion
