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

@ezstart/api-core

v0.2.1

Published

Unified Express-based API server framework (agnostic core + optional @ezstart wrapper)

Readme

@ezstart/api-core

Unified Express-based API server framework — typed responses, Zod validation, OpenAPI-aware router, rate limiting, injected auth. Publishable on npm; works standalone in any project.

Install

pnpm add @ezstart/api-core
# optional peer dependencies
pnpm add @ezstart/config @ezstart/logger    # @ezstart wrapper
pnpm add mongoose                           # only if you ship a MongoDB connector
pnpm add socket.io                          # only if you use createSocketServer

Quickstart (monorepo)

The package ships a pre-configured wrapper wired to @ezstart/config (port + CORS) and @ezstart/logger.

import { Router } from 'express'
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
import { createApiServer, createDocRouter, sendSuccess, startServer } from '@ezstart/api-core'

const { app, config, logger } = createApiServer('myapp')

const registry = new OpenAPIRegistry()
const router = Router()
const docRouter = createDocRouter(registry, router, '/api/items')

docRouter.get('/', (_req, res) => sendSuccess(res, []), { summary: 'List items', tags: ['Items'] })

await startServer(app, {
  routes: router,
  registries: [registry],
  port: config.port,
  serviceName: config.serviceName,
  logger,
})

Quickstart (external / standalone)

Build a fully agnostic server via the low-level createBaseApiServer factory — no @ezstart/* imports required.

import { Router } from 'express'
import {
  createBaseApiServer,
  createAuthMiddleware,
  sendError,
  sendSuccess,
  startServer,
  validateBody,
} from '@ezstart/api-core'
import { z } from 'zod'

const { app, logger } = createBaseApiServer({
  port: 3000,
  serviceName: 'myapp',
  cors: { origins: ['https://myapp.example.com'] },
  rateLimit: { preset: 'standard' },
})

const { requireAuth } = createAuthMiddleware({
  verifyToken: async (token, _kind) => (token === 'good' ? { userId: 'u_1' } : null),
})

const router = Router()
router.post('/echo', requireAuth, validateBody(z.object({ msg: z.string() })), (req, res) => {
  sendSuccess(res, { echoed: req.validatedBody })
})

await startServer(app, { routes: router, port: 3000, logger })

API

createApiServer(appName, opts?): ApiServer

Monorepo wrapper. Uses @ezstart/config (getPort(appName, 'api'), getAllowedOrigins(appName)) and @ezstart/logger by default. Supports the same overrides as createBaseApiServer.

This is the recommended entry point for any consumer that already depends on the @ezstart toolchain.

createBaseApiServer(config): ApiServer

Low-level agnostic factory that returns { app, config, logger }. Wires trust-proxy, CORS, JSON parser, health + root endpoints, and optional global rate limiting. Zero @ezstart/* coupling — publishable on npm as-is.

Key config: port, serviceName, cookieAuthRoutes (path prefixes for cookie-authenticated endpoints) + cookieAuthAllowlist (first-party origins for strict CORS), rateLimit (preset + options), rawBodyRoutes, db, logger. The legacy cors option is still honored for backcompat but is deprecated — see the "CORS policy" section below.

startServer(app, opts): Promise<http.Server>

Bind the app to a port. Mounts routes at basePath, registers /docs (Swagger UI) when registries is provided, awaits db.connect() when a connector is supplied, and wires graceful-shutdown on SIGINT / SIGTERM.

Response helpers — sendSuccess / sendError / sendValidationError

Emit the envelope shapes defined by @ezstart/api-contracts:

  • sendSuccess(res, data, meta?) → { success: true, data, meta? }
  • sendError(res, message, status, { code?, details?, retryAfter? }) → { success: false, error }
  • sendValidationError(res, zodError) → 422 with structured details[]

Middlewares

  • createRateLimiter(opts) (plus createStrictRateLimiter, createVeryStrictRateLimiter, createModerateRateLimiter)
  • createPermissiveCorsMiddleware(opts?) — Tier 1/2 CORS (ACAO: *, credentials: false). Applied globally by createBaseApiServer unless the legacy cors option is passed.
  • createStrictCorsMiddleware({ allowlist, ... }) — Tier 3 CORS for cookie-authenticated routes. Reflects the origin only when it matches the allowlist (exact string or regex) and sets credentials: true.
  • createCorsMiddleware(config) — deprecated wrapper around the cors package. Use the permissive/strict factories instead.
  • createAuthMiddleware({ verifyToken, cookieName? }) — returns { requireAuth, optionalAuth }. The verifier is fully injected (JWT, PASETO, opaque session — all equivalent). Hydrates req.userId and req.user.
  • createApiAuth(jwtSecret?) — monorepo convenience: HS256 JWT verifier wired to JWT_SECRET from the environment, returns { authMiddleware, optionalAuthMiddleware }.
  • validateBody(schema) / validateQuery(schema) / validateParams(schema) — Zod validators that populate req.validatedBody / req.validatedQuery / req.validatedParams.

CORS policy (3-tier)

createApiServer (and the low-level createBaseApiServer) implement a 3-tier CORS model aligned with Stripe / Clerk / Supabase. See .claude/rules/standard-saas-cors.md for the full rationale.

| Tier | Auth mode | Example endpoints | CORS | | ------------ | --------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- | | 1 Public | Publishable key (?key=ez_pk_*) or none | GET /api/keys/config, GET /api/plans, GET /health | ACAO: *, credentials: false | | 2 Bearer | Authorization: Bearer <JWT> | POST /api/donations, GET /api/users/me | ACAO: *, credentials: false | | 3 Cookie | Cookie: session=<httpOnly> (auto-sent by browser) | POST /api/auth/login, POST /api/auth/refresh | ACAO: <reflected origin>, credentials: true, strict allowlist |

Opt-in strict CORS per route prefix via cookieAuthRoutes + cookieAuthAllowlist:

createApiServer('ezauth', {
  cookieAuthRoutes: ['/api/auth/login', '/api/auth/refresh', '/api/auth/logout'],
  cookieAuthAllowlist: [
    'https://ezauth.ezstart.xyz',
    /^https:\/\/ezauth-[a-z0-9]+-ezstart\.vercel\.app$/,
    'http://localhost:6111',
  ],
})

All other paths (Tier 1/2) automatically receive permissive CORS — external consumers can call them from any origin without registering their domain, as expected for a SaaS API.

createDocRouter(registry, router, basePath?): DocRouter

Build an Express router that dual-writes to an OpenAPIRegistry. When basePath is non-empty, a dedicated sub-router is mounted under it so the Express route and the documented OpenAPI path stay in sync.

docRouter.post('/', validateBody(CreateItemSchema), createItemHandler, {
  summary: 'Create item',
  tags: ['Items'],
  bodySchema: CreateItemSchema,
  responseSchema: ItemSchema,
  status: 201,
})

DbConnector interface

interface DbConnector<TModels = unknown> {
  connect(): Promise<void>
  disconnect(): Promise<void>
  readonly models: TModels
  readonly isConnected: boolean
}

Inject your own implementation (MongoDB, Postgres, Redis, ...). startServer awaits connector.connect() before listening and calls connector.disconnect() on shutdown.

createSocketServer(httpServer, config)

Optional Socket.IO helper. The socket.io package is loaded via dynamic import() so consumers who don't use realtime never pay the bundle cost.

Migration from @ezstart/express-core

| Before (@ezstart/express-core) | After (@ezstart/api-core) | | --------------------------------------------------- | -------------------------------------------------------------------------- | | createApp({ apiApp: 'myapp' }) | createApiServer('myapp').app | | createRateLimiter() (+ createStrictRateLimiter) | Same names, re-exported | | createAuthMiddleware(jwtSecret) | createAuthMiddleware({ verifyToken }) — inject any verifier | | createRouterWithDoc(registry, router, basePath) | createDocRouter(registry, router, basePath) | | sendError(res, 'msg', 500, 'CODE') (flat string) | sendError(res, 'msg', 500, { code: 'CODE' }) — structured ErrorPayload | | startServer(app, { port, routes, registries }) | Same shape, now async + supports db, onReady, onShutdown | | connectToMongo('db') | Inject your own DbConnector — core stays agnostic |

See apps/*/api/src/index.ts during the migration window for per-app patches.

Migration from pre-rename names (deprecated, removed in v1.0.0)

| Before | After | | ------------------------------- | -------------------------------------------------- | | createEzstartServer(name, …) | createApiServer(name, …) — monorepo wrapper | | createApiServer(config) (old) | createBaseApiServer(config) — agnostic primitive | | createEzstartAuth(secret?) | createApiAuth(secret?) | | EzstartServerOptions | ApiServerOptions |

Old names remain exported as deprecated aliases for one release cycle. The agnostic createApiServer(config) had a different signature than the new createApiServer(appName, options) — direct consumers of the agnostic factory must rename their import to createBaseApiServer.

Rules

  • Never use raw res.json() for API responses — always go through sendSuccess / sendError / sendValidationError so the client SDK can discriminate envelopes.
  • Never hard-code Authorization parsing — use createAuthMiddleware with a verifier.
  • Logger is silent by default in the agnostic core; opt-in by passing your own logger (@ezstart/logger, pino, winston, ...).

Troubleshooting

CORS returns 500 with empty headers

If your API returns HTTP 500 with no Access-Control-Allow-Origin header on requests carrying an Origin header, check:

  1. Custom error middleware — ensure createErrorHandler is registered as the LAST middleware (after all routers). startServer does this automatically. If you build your server manually with createBaseApiServer and skip startServer, you MUST add app.use(createErrorHandler({ logger })) yourself, AFTER every router. Otherwise unhandled exceptions land in Express's default error path: HTML 500 + zero CORS headers + stack trace leak in production.

  2. OpenTelemetry / Sentry — @sentry/node v10+ ships OpenTelemetry HTTP/Express auto-instrumentation that intercepts setHeader. On managed Node runtimes (Railway in particular) this caused 500 errors on every Origin-bearing request in 2026-04. The CORS middleware in this package is hand-rolled (not the cors npm package) for explicit control. If you re-introduce Sentry: lazy-load it, set defaultIntegrations: false, or migrate to @sentry/node-core (no OTEL bundled).

  3. Helmet crossOriginResourcePolicy — Helmet's default same-origin blocks cross-origin responses. createBaseApiServer ships cross-origin when config.security is enabled (default). If you mount your own helmet config, do the same:

    app.use(helmet({ crossOriginResourcePolicy: { policy: 'cross-origin' } }))

Cookie auth routes refuse cross-origin requests

Tier 3 (cookie-auth) routes use a strict allowlist. If a legitimate caller gets blocked, add their origin to cookieAuthAllowlist (string or regex):

createBaseApiServer({
  port,
  cookieAuthRoutes: ['/api/auth/login', '/api/auth/refresh'],
  cookieAuthAllowlist: ['https://app.example.com', /^https:\/\/preview-[a-z0-9]+\.example\.com$/],
})

Public/Bearer routes are NEVER subject to the allowlist — they always reflect the request origin so any consumer can call them.

Related