@molecule/api-billing-routes
Middleware · billing-routes · API (Node) · v1.1.2 · Apache-2.0
Drop-in /billing/* routes (checkout, cancel, portal) + defineTiers helper. Wires to @molecule/api-payments-stripe via the bond system.
npm install @molecule/api-billing-routesHow it works
@molecule/api-billing-routes is billing-routes middleware for the API, mounted once at startup.
import { createBillingRoutes, defineTiers } from '@molecule/api-billing-routes'
const tiers = defineTiers([
{ id: 'free', name: 'Free', features: ['Up to 5 projects'], priceMonthly: 0 },
{
id: 'pro_monthly',
name: 'Pro (Monthly)',
features: ['Unlimited projects', 'Priority support'],
priceMonthly: 1200,
stripePriceId: process.env.STRIPE_PRICE_PRO_MONTHLY,
},
])
// Mount under /api/billing so the app-side fetches to /api/billing/* line up.
app.use('/api/billing', createBillingRoutes({ tiers }))Works with: @molecule/api-bond, @molecule/api-logger, @molecule/api-payments, @molecule/api-secrets
Secrets: STRIPE_SECRET_KEY, STRIPE_PRICE_PRO_MONTHLY (optional), STRIPE_PRICE_PRO_YEARLY (optional)
Reference
Drop-in /billing/* Express routes + defineTiers() helper for molecule.dev.
Serves the COMPLETE /billing/* contract @molecule/app-billing-react
consumes — GET /tiers, GET /status, POST /checkout, POST /cancel
(plus an optional POST /portal) — so <PricingPage />,
<BillingStatusBadge />, usePricingTiers(), and useBillingStatus() work
against it with no gaps. Replaces the near-identical routes/billing.ts
boilerplate every paid flagship ships. Wires to @molecule/api-payments-stripe
(or any compatible PaymentProvider + createPortalSession) via the bond system.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { createBillingRoutes, defineTiers } from '@molecule/api-billing-routes'
const tiers = defineTiers([
{ id: 'free', name: 'Free', features: ['Up to 5 projects'], priceMonthly: 0 },
{
id: 'pro_monthly',
name: 'Pro (Monthly)',
features: ['Unlimited projects', 'Priority support'],
priceMonthly: 1200,
stripePriceId: process.env.STRIPE_PRICE_PRO_MONTHLY,
},
])
// Mount under /api/billing so the app-side fetches to /api/billing/* line up.
app.use('/api/billing', createBillingRoutes({ tiers }))
Type
middleware
Installation
npm install @molecule/api-billing-routes @molecule/api-bond @molecule/api-logger @molecule/api-payments @molecule/api-secrets express
npm install -D @types/express
API
Interfaces
TierDef
A tier definition supplied to defineTiers(). Each tier represents a single
pricing plan (free, pro monthly, pro yearly, team, etc.).
interface TierDef<TLimits extends Record<string, unknown> = Record<string, unknown>> {
/** Stable identifier for the tier (e.g. `'free'`, `'pro_monthly'`). */
id: string
/** Human-readable label (e.g. `'Pro (Monthly)'`). Not localized — display layer applies i18n. */
name: string
/** Optional monthly price in the smallest currency unit (e.g. cents). `0` means free. */
priceMonthly?: number
/** Optional yearly price in the smallest currency unit (e.g. cents). */
priceYearly?: number
/** Marketing feature bullet keys/strings. The display layer applies i18n. */
features: string[]
/** Optional structured limits enforced at the application layer. */
limits?: TLimits
/** Optional Stripe Price ID — the value passed to `createCheckoutSession`. */
stripePriceId?: string
}
TierAccessors
Accessors returned by defineTiers(). These are pure (no side effects) and
typed against the same TLimits parameter as the input.
interface TierAccessors<TLimits extends Record<string, unknown> = Record<string, unknown>> {
/** Returns the full list of tiers in the order they were declared. */
getPricingTiers(): ReadonlyArray<TierDef<TLimits>>
/** Returns the tier whose `id` matches, or `undefined`. */
getTierById(id: string): TierDef<TLimits> | undefined
/** Returns the tier whose `id` matches; throws if not found. */
requireTier(id: string): TierDef<TLimits>
/**
* Returns `true` iff `tierId` is the same tier as `minimumId` or appears
* after it in the declaration order. Useful for capability gates like
* `tierAtLeast(currentId, 'pro_monthly')`.
*/
tierAtLeast(tierId: string, minimumId: string): boolean
}
PortalSessionResult
Result of creating a Stripe-style customer-portal session.
interface PortalSessionResult {
/** The portal session ID. */
id: string
/** The URL to redirect the user to. */
url: string
}
BillingProvider
Provider methods consumed by the billing routes. This is a structural
superset of PaymentProvider — any provider that supplies these methods
is compatible. The Stripe bond satisfies cancelSubscription and (via
updateSubscription) checkout-session creation directly. Portal session
creation is optional; if omitted, POST /billing/portal returns 501.
interface BillingProvider extends PaymentProvider {
/**
* Creates a customer-portal session. Optional — Stripe-style providers
* typically expose this; receipt-based providers (Apple/Google) do not.
*/
createPortalSession?(params: {
userId: string
returnUrl?: string
}): Promise<PortalSessionResult | null>
}
BillingErrorBody
Shape of a serializable error response from the billing routes.
interface BillingErrorBody {
/** Stable, machine-readable error code. */
code: string
/** English fallback message. Apps may localize via response interceptors. */
message: string
}
PricingPrice
One billing-period price row in the public pricing table.
Mirrors @molecule/app-billing-react's PricingTierPrice — the shape the
<PricingPage /> / usePricingTiers() frontend consumes verbatim. Kept as a
local re-declaration because API packages must not import @molecule/app-*
(cross-stack boundary), so the contract is asserted structurally on both sides.
interface PricingPrice {
/** Billing cadence. */
period: 'month' | 'year'
/** Display string (e.g. `'$19/mo'`, `'$190/yr'`, `'$0'`). */
price: string
/**
* Stripe Price ID used as the checkout line item. `null` for the free tier
* (and during local dev when the price-id env vars are unset).
*/
stripePriceId: string | null
/** Optional savings tag on yearly variants (e.g. `'2 months free'`). */
savings?: string
}
PricingTierEntry
One row of the public pricing table returned by GET /tiers.
Mirrors @molecule/app-billing-react's PricingTierEntry. When the derive
path is used, each TierDef maps to exactly one entry with a single price
(the TierDef model treats each billing period as its own tier). Supply
getPricingTiers in the options to serve a richer catalogue instead.
interface PricingTierEntry<TLimits = unknown> {
/** Stable slug used as the row key (matches the source `TierDef.id`). */
key: string
/** Display name shown in the page heading and CTA. */
name: string
/** Price variants for the tier. Presentation order. */
prices: PricingPrice[]
/** Tier-specific limits rendered on the comparison row. */
limits: TLimits
/** Whether the tier is billed per seat (rendered as a footnote). */
perSeat?: boolean
}
PricingTiersResponse
Response envelope for GET /tiers.
Mirrors @molecule/app-billing-react's PricingTiersResponse — the frontend
reads data directly, so the envelope key must be data.
interface PricingTiersResponse<TLimits = unknown> {
/** Tiers ordered as declared (typically free → pro → team). */
data: PricingTierEntry<TLimits>[]
}
BillingStatusResponse
Snapshot of the signed-in user's current billing state, returned by
GET /status.
Mirrors @molecule/app-billing-react's BillingStatus — the same shape
@molecule/api-bonds-default-express's createBillingRouter emits from
@molecule/api-entitlements's getEffectiveTier, so the two are drop-in
interchangeable for the frontend.
interface BillingStatusResponse<TLimits = unknown> {
/** The user's `users.planKey` (`'free'`, `'stripeMonthly'`, etc.). */
planKey: string
/** Tier category — `'free'`, `'pro'`, `'team'`, or any app-defined extension. */
category: string
/** Display name (matches the active tier's `PricingTierEntry.name`). */
name: string
/** Tier-specific limits the user is currently entitled to. */
limits: TLimits
/** True when the user is on the default (free) tier. */
isFree: boolean
}
BillingRoutesOptions
Options accepted by createBillingRoutes().
interface BillingRoutesOptions<TLimits extends Record<string, unknown> = Record<string, unknown>> {
/** The tier accessors returned by `defineTiers()`. */
tiers: TierAccessors<TLimits>
/**
* Optional injected provider — if omitted, the routes look up
* `bond('payments', providerName)`.
*/
provider?: BillingProvider
/**
* Bonded provider name. Defaults to `'stripe'`.
*/
providerName?: string
/**
* Resolves the authenticated user ID from a request. Defaults to reading
* `res.locals.session.userId` (Express convention).
*/
resolveUserId?: AuthResolver
/**
* Optional override that returns the public pricing table in the frontend's
* `PricingTierEntry` shape. When omitted, `GET /tiers` derives one entry per
* configured `TierDef`. Supply this to serve a richer catalogue (multiple
* period variants per tier, `savings` tags, `perSeat`) — it is also the hook
* `@molecule/api-bonds-default-express` uses to delegate its `/tiers` here.
*/
getPricingTiers?: () => ReadonlyArray<PricingTierEntry<TLimits>>
/**
* Optional formatter turning a `TierDef` price (smallest currency unit) into
* the display string `GET /tiers` emits. Defaults to a USD formatter
* (`$12/mo`, `$120/yr`, `$0`). Only used by the derive path — ignored when
* `getPricingTiers` is supplied.
*/
formatPrice?: PriceFormatter
/**
* Resolves the authenticated user's current billing status for `GET /status`.
* Injectable so the routes stay decoupled from any specific entitlements
* store. When omitted (or when it returns `null`), `GET /status` returns the
* default (first / free) tier snapshot for any authenticated user.
*/
resolveStatus?: BillingStatusResolver<TLimits>
/**
* Optional override for `SubscriptionUpdateResult` shape used in tests.
* @internal
*/
__subscriptionUpdateResult?: SubscriptionUpdateResult
}
Types
PriceFormatter
Formats a price expressed in the smallest currency unit (e.g. cents) into
the display string GET /tiers emits for a PricingPrice.
type PriceFormatter = (amountMinor: number, period: 'month' | 'year') => string
amountMinor— Price in the smallest currency unit (e.g.1200= $12.00).period— The billing cadence the amount is for.
Returns: A display string (e.g. '$12/mo', '$120/yr', '$0').
BillingStatusResolver
Resolves the authenticated user's current billing status for GET /status.
Kept injectable so this package stays decoupled from any specific
entitlements / tier store: flagship apps pass a resolver backed by
@molecule/api-entitlements's getEffectiveTier. Returning null (or not
supplying a resolver at all) makes GET /status fall back to the default
(first / free) tier snapshot derived from the configured tiers.
type BillingStatusResolver<TLimits = unknown> = (
userId: string,
req: unknown,
res: unknown,
) => BillingStatusResponse<TLimits> | null | Promise<BillingStatusResponse<TLimits> | null>
AuthResolver
Result of resolving the authenticated user from a request.
type AuthResolver = (req: unknown, res: unknown) => string | null | Promise<string | null>
Functions
createBillingRoutes(options)
Creates a drop-in Express Router exposing the standard billing routes —
the exact /billing/* contract @molecule/app-billing-react
(usePricingTiers / useBillingStatus / <PricingPage /> /
<BillingStatusBadge />) consumes.
Mounted routes:
GET /tiers— public pricing table. Returns{ data: PricingTierEntry[] }. Derives one entry per configuredTierDefby default, or servesoptions.getPricingTiers()verbatim when supplied. No auth.GET /status— the signed-in user's current tier snapshot{ planKey, category, name, limits, isFree }. Returns 401 when unauthenticated. Usesoptions.resolveStatuswhen supplied; otherwise returns the default (first / free) tier snapshot.POST /checkout— body{ priceId: string }. Callsprovider.updateSubscription({ userId, newProductId: priceId }). Returns{ checkoutUrl }for new subscriptions or{ updated, subscription }for in-place plan changes.POST /cancel— no body. Callsprovider.cancelSubscription({ userId })and returns{ canceled: true }on success.POST /portal— body{ returnPath?: string }(an app-relative path such as/billing; a legacyreturnUrlis accepted only when it is already on the app origin, since the provider renders it as a link and an arbitrary one would be an open redirect). Callsprovider.createPortalSession({ userId, returnUrl })and returns{ url }. Returns 501 if the bonded provider does not implement portal sessions.
(description truncated — read the full text in node_modules/@molecule/api-billing-routes/dist/routes.d.ts.)
function createBillingRoutes(options: BillingRoutesOptions<TLimits>): ExpressRouter
options— Tier accessors plus optional provider/auth/pricing/status overrides.
Returns: An Express Router ready to be mounted.
defineTiers(tiers)
Builds a TierAccessors object from an ordered list of tier definitions.
Validates that the list is non-empty and that all id values are unique.
The declaration order of the array IS the upgrade order — tierAtLeast
uses index comparison, so list cheaper tiers first.
function defineTiers(tiers: readonly TierDef<TLimits>[]): TierAccessors<TLimits>
tiers— Ordered list of tier definitions, cheapest → most expensive.
Returns: Read-only accessors keyed off the supplied tier ids.
Constants
billingRoutesSecretDefinitions
Secret definitions required by the billing routes middleware.
const billingRoutesSecretDefinitions: SecretDefinition[]
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1@molecule/api-logger^1.0.1@molecule/api-payments^1.0.1@molecule/api-secrets^1.0.1express^4.0.0 || ^5.0.0
Environment Variables
STRIPE_SECRET_KEY(required) — Stripe secret key- Setup: Stripe Dashboard → Developers → API keys; use the sk_test_ key in test mode, sk_live_ in production.
- Get it here: https://dashboard.stripe.com/apikeys
- Example:
sk_test_...
STRIPE_PRICE_PRO_MONTHLY(optional) — Stripe price ID (Pro monthly)- Setup: Stripe Dashboard > Products > your Pro product > Add price (recurring monthly); copy the price_... ID.
- Get it here: https://dashboard.stripe.com/products
- Example:
price_...
STRIPE_PRICE_PRO_YEARLY(optional) — Stripe price ID (Pro yearly)- Setup: Stripe Dashboard > Products > your Pro product > Add price (recurring yearly); copy the price_... ID.
- Get it here: https://dashboard.stripe.com/products
- Example:
price_...
Runtime Dependencies
-
@molecule/api-bond -
@molecule/api-logger -
@molecule/api-payments -
@molecule/api-secrets -
express -
The package looks up
bond('payments', 'stripe')by default; passproviderorproviderNameto override. -
Routes served:
GET /tiers,GET /status,POST /checkout,POST /cancel,POST /portal.GET /tiersreturns{ data: PricingTierEntry[] }— derived one-per-TierDefby default, oroptions.getPricingTiers()verbatim for a richer catalogue.GET /statusreturns{ planKey, category, name, limits, isFree }— passoptions.resolveStatus(e.g. backed by@molecule/api-entitlements'sgetEffectiveTier) to report the user's real plan; without it the default free-tier snapshot is returned for any authenticated user. -
Mount so the resulting app-facing paths are
/api/billing/...— the scaffolded Vite dev proxy forwards/apiWITHOUT rewriting. -
Naming:
@molecule/api-entitlementsexports a DIFFERENTdefineTiers(registry-object signature). Do not mix the two in one file without aliasing. -
Error responses use the
BillingErrorBodyshape ({ code, message }) — apps may layer i18n on top via response interceptors.