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

@startsimpli/api

v0.5.30

Published

Type-safe Django REST API client for StartSimpli apps

Readme

@startsimpli/api

Type-safe HTTP client + per-domain wrappers for the Django backend (start-simpli-api). One package, used by every Next.js app in the monorepo: raise-simpli, market-simpli, trade-simpli, vault-web.

No Prisma, no DB drivers — this is the only sanctioned way for a Next.js app in this monorepo to talk to Django.

Install

This is a workspace package; consumers depend on it via pnpm workspaces and Next.js needs to transpile its TS source.

// app's package.json
{
  "dependencies": {
    "@startsimpli/api": "workspace:*"
  }
}
// app's next.config.ts — REQUIRED so Next compiles the package's src/
const config = {
  transpilePackages: [
    '@startsimpli/api',
    // … other @startsimpli/* packages
  ],
};

The package's main/types point at src/index.ts (no build step), so without transpilePackages Next will reject the TypeScript.

Quick start

// src/lib/api.ts (real shape used by trade-simpli and vault-web)
import { createStartSimpliApi } from '@startsimpli/api';
import { getRegisteredToken } from '@/infrastructure/auth';

export const api = createStartSimpliApi({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || '',
  getToken: () => getRegisteredToken(),
  onUnauthorized: () => {
    if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
      window.location.href = '/login';
    }
  },
});

export type Api = typeof api;

createStartSimpliApi returns an object with one wrapper per domain plus the underlying client:

api.contacts        // ContactsApi
api.organizations   // OrganizationsApi
api.entities        // EntitiesApi (low-level tags / metrics / profiles / attributes)
api.workflows       // WorkflowsApi
api.messages        // MessagesApi
api.messageTemplates// MessageTemplatesApi
api.funnels         // FunnelsApi
api.featureFlags    // FeatureFlagsApi
api.users           // UsersApi
api.enrichment      // EnrichmentApi (Apollo)
api.targetLists     // TargetListsApi
api.markets         // MarketsApi (trade-simpli)
api.vault           // VaultApi (vault-web)
api.client          // raw ApiClient — escape hatch for custom endpoints

Every config field is optional. baseUrl: '' makes calls relative so Next.js rewrites/proxy routes can intercept them.

Public surface

Grouped by purpose. See src/index.ts for the full export list.

| Area | Exports | |---|---| | Factory | createStartSimpliApi | | Core HTTP | ApiClient, createApiClient, FetchWrapper | | Domain wrappers | ContactsApi, OrganizationsApi, EntitiesApi, WorkflowsApi, MessagesApi, MessageTemplatesApi, FunnelsApi, FeatureFlagsApi, UsersApi, EnrichmentApi, TargetListsApi, MarketsApi, VaultApi | | Generic entity adapter | createEntityAdapter | | Error contract (HTTP layer) | ApiException, parseErrorResponse, handleFetchError, isApiException, isValidationError, isAuthError, isNotFoundError | | Application error hierarchy (server-side domain) | AppError, AppErrorCode, ValidationError, AuthenticationError, AuthorizationError, NotFoundError, ConflictError, RateLimitError, DatabaseError, ExternalServiceError, isPrismaError, toAppError | | Sanitization | sanitizeHtml, sanitizeSearchQuery, validateIdentifier, sanitizeUserInput, sanitizeChatMessage, MAX_PROMPT_LENGTH, MAX_CHAT_MESSAGE_LENGTH | | CORS | getCorsHeaders, applyCorsHeaders, createCorsMiddleware | | Rate limiting | createRateLimiter, getClientIP | | Server cache | CacheStore, CacheManager, getCacheManager, resetCacheManager | | Feature flags React glue | FeatureFlagProvider, useFeatureFlags | | Server-side data hooks | useServerList, useServerDetail | | Funnel guards | isFunnelRunConflict, isFunnelValidationError | | Message stats helper | calculateStatsFromMessages | | Env helpers | getRequiredEnv, getOptionalEnv, validateEnvVars | | Constants | ENDPOINTS, COMPANY_SIZE_OPTIONS, LIFECYCLE_STAGE_OPTIONS, REVENUE_RANGE_OPTIONS, ACTIVITY_TYPE_OPTIONS, ACTIVITY_OUTCOME_OPTIONS, LOSS_REASON_OPTIONS, DEAL_STAGE_OPTIONS | | Utilities | re-exported from ./utils (buildFilterParams, mergeQueryParams, case-transform helpers, etc.) | | Types | re-exported from ./types (Contact, Organization, Entity, Tag, Metric, Profile, Attribute, PaginatedResponse, ApiError, all per-domain Create/Update/Filter shapes) |

Middleware (withAuth, withValidation, withErrorHandling) is not in the main entry — import it from @startsimpli/api/middleware to keep server-only Next deps out of client/test bundles.

Per-domain wrappers

All wrapper return values are camelCase. Filters/inputs are camelCase too — the fetch layer snake-cases them before they hit Django (see "snake↔camel" below).

ContactsApi/api/v1/contacts/

Generic CRUD over the Contact entity, with assertion-based writes (writeTags, writeMetrics, writeProfiles).

// market-simpli/src/modules/prospects/api/client.ts
const response = await api.contacts.list(params);
const created = await api.contacts.create({ name, email, writeTags: ['tier_1'] });
await api.contacts.update(id, data);
await api.contacts.delete(id);

Also: get, search, getByFirm.

OrganizationsApi/api/v1/organizations/

Same shape as ContactsApi but for orgs/firms, with extra range helpers (getByCheckSizeRange, getByStage).

EntitiesApi/api/v1/core/

Low-level access to the generic assertion store: Tags, EntityTags, Metrics, Profiles, Attributes, Relationships. Use when you need to query assertions directly instead of through a typed entity wrapper.

const tags = await api.entities.listTags();
const metrics = await api.entities.listMetrics(undefined, { entityId, type: 'financial' });

FunnelsApi/api/v1/funnels/

Lead/deal funnels: list/create/update/delete, plus run, preview, cancelRun, getRuns, getResults, listTemplates, getFields, and stage operations (addEntity, move, stats, pipeline).

// market-simpli/src/shared/lib/api/funnels.ts
const response = await api.funnels.list({ tags });
const run = await api.funnels.run(funnelId);
const runs = await api.funnels.getRuns(funnelId);

Ships two type guards for known failure modes: isFunnelRunConflict(err) (409 on already-running) and isFunnelValidationError(err) (400 with field errors).

MessagesApi + MessageTemplatesApi/api/v1/messages/, /api/v1/message-templates/

Outbound messaging campaigns: drafts, scheduling, sending, test sends, per-recipient tracking, channel discovery.

// market-simpli/src/app/(dashboard)/email/compose/page.tsx
const message = draftId
  ? await api.messages.update(draftId, payload)
  : await api.messages.create(payload);

await api.messages.sendNow(message.id);

const [lists, channels] = await Promise.all([
  api.targetLists.list({ pageSize: 100 }),
  api.messages.getChannels(),
]);

calculateStatsFromMessages(messages) computes aggregate open/click/reply/bounce rates client-side from a list.

TargetListsApi/api/v1/targets/lists/

Reusable contact/organization lists. CRUD plus member ops (getMembers, addMembers, removeMembers, refresh). Supports static, funnel-backed, and query-backed lists.

const members = await api.targetLists.getMembers(id, { pageSize: 500 });

EnrichmentApi/api/v1/enrichment/, /api/v1/contacts/<id>/enrich/

Apollo enrichment. The backend caps enrich-apollo at 100 ids/request; enrichApollo(ids, { onProgress }) automatically chunks the input, calls the endpoint sequentially, and returns a single aggregated ApolloEnrichmentSummary. Partial failures are recorded per id without aborting the run.

UsersApi/api/v1/users/

Three calls: getProfile, updateProfile, changePassword. Used by every app's account settings.

// market-simpli/src/app/(dashboard)/settings/account/page.tsx
const profile = await api.users.getProfile();
await api.users.updateProfile({ firstName, lastName });
await api.users.changePassword({ oldPassword, newPassword });

WorkflowsApi/api/v1/workflows/

Workflow definitions + execution history. CRUD on Workflow, plus WorkflowExecution reads with the standard status / mode filters.

FeatureFlagsApi + <FeatureFlagProvider> + useFeatureFlags()

Server-side flags loaded once at app boot, fed into React via context. Apps mount FeatureFlagProvider near the root and read flags with useFeatureFlags().

MarketsApi/api/v1/markets/*

Backs trade-simpli. Instruments, OHLCV bars, analytics (returns/volatility/beta/pair_spread), per-instrument news, options chains + IV/Greeks/unusual-activity/ATM IV history, earnings calendar, macro calendar, sector breadth, VIX term structure, social posts, trading snapshots, and an ops health rollup.

// trade-simpli/src/app/(dashboard)/options/[symbol]/page.tsx
api.markets.getOptionsChain({ symbol, expiry })
api.markets.getOptionsIvHistory({ symbol })
api.markets.getOptionsUnusualActivity({ symbols: [symbol], lookback: 20 })

// trade-simpli/src/app/(dashboard)/pairs/[name]/page.tsx
api.markets.getTradingSnapshots({ limit: 90 })
api.markets.getAnalytics(pair.bull, { metric: 'pair_spread', vs: pair.bear, windowDays: 90 })
api.markets.getNews(pair.bull, { limit: 10, minConfidence: 0.3 })

VaultApi/api/v1/vault/*

Backs vault-web. Environments, secrets (value is write-only on create/update; readback requires the separate audited revealSecret), access keys, and an audit log per environment.

// vault-web/src/app/(dashboard)/environments/page.tsx — via hooks that wrap api.vault
const { data } = useEnvironments(api.vault);
const createEnv = useCreateEnvironment(api.vault);

// raw equivalents:
await api.vault.listEnvironments();
await api.vault.createEnvironment({ slug, name });
await api.vault.revealSecret(envSlug, secretId);

Error contract

All HTTP failures throw ApiException. The backend (apps.core.exceptions) now emits a standardized error response with a machine code and structured details, and parseErrorResponse lifts those onto the thrown exception:

class ApiException extends Error {
  status?: number;              // HTTP status
  statusText?: string;
  detail?: string;              // DRF { detail: ... } string when present
  errors?: Record<string, string[]>; // DRF field-level validation errors
  code?: string;                // NEW: machine code, e.g. 'limit_reached', 'no_company'
  details?: Record<string, unknown>; // NEW: extra context — feature_key, limit, current, …
}

parseErrorResponse recognises three response shapes and normalises them onto the same exception:

  1. Standardized (preferred){ error, code, statusCode, fieldErrors?, …extras }. The human error becomes .message, code is preserved on .code, and every non-reserved key is stapled onto .details. Reserved keys (error, code, statusCode/status_code, fieldErrors/field_errors, timestamp, requestId/request_id, detail) are excluded from .details.
  2. DRF detail{ detail: '...' }.detail + .message.
  3. DRF field errors{ field: ['msg', …] }.errors.

Discriminating on code (the key new contract)

// vault-web/src/app/(dashboard)/environments/page.tsx
import { ApiException } from '@startsimpli/api';

function limitErrorMessage(err: unknown): string | null {
  if (err instanceof ApiException && err.code === 'limit_reached') {
    // err.details carries { featureKey, limit, current } from the backend
    return err.message || "You've reached the Free plan limit. Upgrade for unlimited environments.";
  }
  return null;
}

Known codes (server emits these via apps.core.exceptions):

  • limit_reached — billing/quota gate; details carries featureKey, limit, current. Vault-web renders the upgrade nudge from this.
  • no_company — user has no active company context selected.
  • Plus any code the backend defines on apps.core.exceptions subclasses.

Status-based guards (still supported)

import {
  isApiException,
  isValidationError,  // status === 400 && has .errors
  isAuthError,        // status === 401 || 403
  isNotFoundError,    // status === 404
} from '@startsimpli/api';

try {
  await api.contacts.get(id);
} catch (err) {
  if (isValidationError(err)) console.log(err.errors);
  else if (isAuthError(err))   /* redirect via onUnauthorized */;
  else if (isNotFoundError(err)) /* show empty state */;
  else if (isApiException(err)) console.error(err.code, err.message);
  else throw err;
}

Server-side domain error hierarchy

Separate from ApiException, the package also exports a Node-side error hierarchy (AppError, ValidationError, AuthenticationError, …, plus toAppError and isPrismaError) for Next.js route handlers / server actions that want to throw typed errors and have middleware (withErrorHandling) format them. Use these in server code; use ApiException everywhere a fetch can fail.

snake↔camel transform

Django speaks snake_case; this package speaks camelCase. The transform is on by default:

  • Responses: snakeToCamel(body) is applied to the parsed JSON before it's returned.
  • Request bodies: camelToSnake(body) is applied before JSON.stringify.
  • Query params: built by buildFilterParams / mergeQueryParams — pass camelCase; they're emitted as snake_case.

Opt out per-client when you need raw keys (e.g. an endpoint that already returns camelCase, or one that requires exact-key passthrough):

import { createStartSimpliApi } from '@startsimpli/api';

const api = createStartSimpliApi({
  baseUrl: process.env.NEXT_PUBLIC_API_URL,
  getToken,
  transformKeys: false, // disable both directions for this client
});

Note: a few Django endpoints (notably some billing endpoints under /billing/* proxied through Next route handlers) emit raw snake_case and bypass the DRF camelCase middleware. Apps that proxy those typically pass fromSnake: true in their own proxy helpers — that's an app-level concern, not something transformKeys toggles.

Next.js API route middleware

Imported from a separate entry to avoid pulling server deps into the main bundle:

import { withAuth, withValidation, withErrorHandling } from '@startsimpli/api/middleware';
import { z } from 'zod';

const schema = z.object({ name: z.string() });

export const POST = withErrorHandling(
  withAuth(
    withValidation(
      async (request, { body }, authContext) => {
        return NextResponse.json({ ok: true });
      },
      { body: schema },
    ),
  ),
);

withAuth injects { userId, token, isAuthenticated }. withErrorHandling catches thrown AppError / ApiException and formats them into a standard JSON response.

Direct client escape hatch

For endpoints without a wrapper:

import { createApiClient } from '@startsimpli/api';

const client = createApiClient({ baseUrl, getToken });

const data = await client.fetch.get('/custom-endpoint/', { params: { key: 'value' } });
const created = await client.fetch.post('/custom-endpoint/', { name: 'test' });

client.fetch is the FetchWrapper; it carries the auth token, runs the key transform, and throws ApiException on failure exactly like the typed wrappers.

Verification

pnpm --filter @startsimpli/api test          # vitest, 12 files, 133 tests
pnpm --filter @startsimpli/api type-check    # tsc --noEmit

Test coverage at the time of writing: 12 test files across src/__tests__/ and src/lib/__tests__/, totalling 133 passing tests. Headline suites: DRF camel/snake transforms, URL building, query-param shaping, JWT refresh, paginated fetchAllPages, no-token-on-unsafe-method guard, response schema validation, entity adapter, entity query builder, funnels API contracts, enrichment chunking, and the Vault API surface.

Shared-first policy

If you find yourself writing an HTTP wrapper, an api-client helper, an error-shape util, or a query-param builder inside an app's src/, stop — extend this package instead. App code should call api.<wrapper>.<method>(), not build its own fetcher. See the monorepo CLAUDE.md rule 9.

Architecture notes

  • Base URL: configurable via baseUrl; '' (default) uses relative paths so Next.js rewrites/proxies can intercept.
  • Auth: Authorization: Bearer <token> from getToken(); the wrapper handles refresh via onTokenRefresh and one-shot dedup of onUnauthorized callbacks (5s window).
  • Pagination: DRF format — page, pageSize (sent as page_size).
  • Filtering: Django-filter conventions — field__gte, field__in, etc. — written camelCase from callers.
  • Entity assertion model: Django stores generic Tag / Metric / Profile / Attribute assertions against entities; writes use writeTags / writeMetrics / writeProfiles, reads include both canonical fields and the raw assertion arrays.

License

MIT