@thejob/marketing-client
v1.2.0
Published
Typed TypeScript/JavaScript client for the thejob-marketing-service HTTP API.
Readme
@thejob/marketing-client
Typed client for thejob-marketing-service: mailing lists, campaigns, and the configuration behind them.
import { MarketingClient } from '@thejob/marketing-client';
const marketing = new MarketingClient({
baseUrl: process.env.MARKETING_SERVICE_URL!,
token: adminJwt,
});
const { items, total } = await marketing.listCampaigns({ page: 1, limit: 25 });Auth is an admin JWT, and this is a server-side client
Every endpoint here is requireAdmin. There is no API-key alternative, because
marketing has no service-to-service surface — nothing else on the platform sends
mail on its own account.
So construct this on the server, never in a browser. In svelte-ui that means
inside a proxy endpoint, with locals.jwt as token. Handing this client a
token in the browser puts an admin JWT in front of the user.
What it does not cover
Three route groups are deliberately absent, each for its own reason:
| Route | Why not |
|---|---|
| /dashboard/* | Marketing's own ops page, gated by a session cookie, not a bearer token. This client cannot authenticate to it, and it is a rendered admin surface rather than an API. |
| POST /internal/email-status | A signed callback from email-service, which computes the signature. A generic method would only invite an unsigned call that 401s. |
| GET /unsubscribe | Public, takes a token from a mail link, answers HTML to a human. |
Everything else is covered, and a test enforces it: it drives a fake fetch through every public method, compares what was requested against the routes the service actually registers, and fails in both directions — a route with no method, and a method calling a path the service does not register.
Pages are items, not results
marketing-service answers { items, total, page, limit }. jobs-service and
user-service answer results. That difference is preserved here rather than
harmonised: renaming it would make this client disagree with the service it
describes, and the next person debugging a response would have no way to
reconcile the two.
Types come from @thejob/schema
Campaign, MailingList, MailingListFilter and TemplateVariableBinding are
derived with InferType from the schemas that validate them, never re-declared.
A hand-written copy of a schema is exactly the thing that silently stops
matching it — svelte-ui carried a RecipientPreview whose own comment asked the
reader to "keep the three in sync".
Sending is asynchronous
sendCampaign answers 202. email-service sends in the background and reports
progress to a signed webhook, so a returned batchId means accepted, not
delivered.
A send can also be refused: if the four send switches do not all agree, the
service answers 200 with blockedBy set rather than failing. That is the safety
working, not an error, so check the field rather than relying on a throw.
