@periodic/strontium-next
v1.0.0
Published
Production-grade Next.js integration for @periodic/strontium — App Router, Pages Router, Edge, and Middleware all covered
Maintainers
Readme
🔺 Periodic Strontium Next
Production-grade Next.js integration for @periodic/strontium — App Router, Pages Router, Edge, and Middleware all covered
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Strontium Next?
@periodic/strontium-next is the Next.js integration layer for @periodic/strontium. It brings Strontium's resilience primitives — retry, circuit breaking, deduplication, and timeout control — into every corner of your Next.js application: Server Components, Route Handlers, Middleware, Edge functions, and the Pages Router.
Next.js introduces unique HTTP challenges that generic clients don't address. Headers need to be forwarded from the incoming request to upstream services. Errors need to be mapped to HTTP status codes at the route handler boundary. Cache tags need to be attached to requests and invalidated after mutations. Edge runtime compatibility rules out Node.js-specific APIs. Strontium Next handles all of it.
The name represents:
- Coverage: Every Next.js runtime and routing system is supported
- Transparency: Trace headers, correlation IDs, and auth tokens flow through automatically
- Precision: Strontium errors map to correct HTTP status codes at every boundary
- Simplicity: One package, one pattern, regardless of which Next.js feature you're using
Just as @periodic/strontium makes your HTTP layer reliable, @periodic/strontium-next makes that reliability native to Next.js — without fighting the framework.
🎯 Why Choose Strontium Next?
Next.js applications make HTTP calls in more places than most frameworks — and generic HTTP clients don't account for any of them:
- Server Components need trace headers from the incoming request forwarded automatically
- Route Handlers need Strontium errors mapped to correct HTTP status codes — not unhandled 500s
- Middleware needs idempotency keys and trace context injected before requests reach handlers
- Edge runtime rules out Node.js APIs, breaking most HTTP client solutions
- ISR / cache tags need to be attached to requests and invalidated after mutations
- Pages Router needs a different client setup than App Router
Periodic Strontium Next provides the perfect solution:
✅ Zero extra dependencies — requires only @periodic/strontium and Next.js
✅ App Router — Server Components and Route Handlers fully supported
✅ Pages Router — works identically in API routes
✅ Edge runtime safe — no Node.js APIs, no process usage
✅ Automatic header forwarding — trace IDs, auth tokens, correlation IDs pass through
✅ wrapRouteHandler — maps Strontium errors to correct HTTP status codes
✅ Middleware support — inject request IDs, idempotency keys, and trace context
✅ Cache integration — taggedRequest and strontiumRevalidateTag for Next.js ISR
✅ Hydration-safe singleton — getClientStrontiumClient for browser
✅ Type-safe — strict TypeScript, zero any, throughout
✅ No global state — no side effects on import
✅ Production-ready — non-blocking, never crashes your app
📦 Installation
npm install @periodic/strontium-next @periodic/strontiumOr with yarn:
yarn add @periodic/strontium-next @periodic/strontium🚀 Quick Start
// app/users/[id]/page.tsx
import { createServerStrontiumClient } from '@periodic/strontium-next';
import { headers } from 'next/headers';
export default async function UserPage({ params }: { params: { id: string } }) {
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: new Request('https://placeholder', {
headers: Object.fromEntries(headers().entries()),
}),
});
const res = await client.request<User>({
method: 'GET',
url: `/users/${params.id}`,
});
return <UserProfile user={res.data} />;
}🧠 Core Concepts
createServerStrontiumClient
- The primary factory for server-side usage
- Accepts the incoming
RequestorNextRequestand forwards trace headers automatically - Forwards
x-request-id,x-correlation-id,traceparent,authorization, and other trace headers - All core Strontium config options (retry, circuit breaker, timeout) are supported
- Use this in Server Components, Route Handlers, and API routes
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: req, // incoming request — headers forwarded automatically
retry: { enabled: true, maxAttempts: 3, strategy: 'exponential' },
circuitBreaker: { failureThreshold: 5, resetTimeoutMs: 30_000 },
});wrapRouteHandler
- Maps Strontium errors to correct HTTP status codes at the route boundary
- No more unhandled
500responses when the circuit opens or a timeout fires - Wraps any App Router route handler function
Design principle:
Errors should be handled at the boundary, not scattered across every handler.
wrapRouteHandleris that boundary.
| Error | Status Code |
|-------|-------------|
| CircuitOpenError | 503 + Retry-After: 30 |
| TimeoutError | 504 |
| RetryExhaustedError | 502 |
| ResponseValidationError | 502 |
| IntegrityViolationError | 409 |
| NetworkError | 502 |
| Unknown | 500 |
✨ Features
🖥️ App Router: Server Components
Fetch data in Server Components with automatic header forwarding:
// app/orders/page.tsx
import { createServerStrontiumClient } from '@periodic/strontium-next';
import { headers } from 'next/headers';
export default async function OrdersPage() {
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: new Request('https://placeholder', {
headers: Object.fromEntries(headers().entries()),
}),
});
const res = await client.request<Order[]>({ method: 'GET', url: '/orders' });
return <OrderList orders={res.data} />;
}🛣️ App Router: Route Handlers
Wrap route handlers to get automatic error-to-status-code mapping:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerStrontiumClient, wrapRouteHandler, taggedRequest } from '@periodic/strontium-next';
export const GET = wrapRouteHandler(async (req: NextRequest) => {
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: req,
});
const res = await client.request<UsersResponse>(
taggedRequest({ method: 'GET', url: '/users' }, ['users'])
);
return NextResponse.json(res.data);
});🔀 Middleware
Inject request IDs, idempotency keys, and trace context before requests reach your handlers:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { createStrontiumMiddleware } from '@periodic/strontium-next';
const strontiumMiddleware = createStrontiumMiddleware({
idempotencyNamespace: 'myapp',
otelPropagation: true,
});
export function middleware(request: NextRequest) {
return strontiumMiddleware(request, () => Promise.resolve(NextResponse.next()));
}
export const config = { matcher: '/api/:path*' };⚡ Edge Runtime
Full support for the Edge runtime — no Node.js APIs required:
// app/api/edge/route.ts
export const runtime = 'edge';
import { createEdgeStrontiumClient } from '@periodic/strontium-next/edge';
export async function GET(request: Request) {
const client = createEdgeStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request,
});
const res = await client.request<Data>({ method: 'GET', url: '/data' });
return Response.json(res.data);
}📄 Pages Router
Works identically in Pages Router API routes:
// pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createServerStrontiumClient } from '@periodic/strontium-next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const client = createServerStrontiumClient({ baseURL: process.env.API_BASE_URL! });
const result = await client.request<User>({ method: 'GET', url: `/users/${req.query.id}` });
res.status(200).json(result.data);
}🏷️ Cache Integration
Attach Next.js ISR cache tags to requests and invalidate them after mutations:
import { taggedRequest, strontiumRevalidateTag, strontiumRevalidateTags } from '@periodic/strontium-next';
// Attach cache tags to a request
const res = await client.request<User[]>(
taggedRequest({ method: 'GET', url: '/users' }, ['users'])
);
// Invalidate after a mutation
await strontiumRevalidateTag('users');
await strontiumRevalidateTags(['users', `user-${id}`]);🌐 Hydration-Safe Browser Client
A singleton client for browser-side usage that's safe across hydration:
import { getClientStrontiumClient } from '@periodic/strontium-next';
// Returns the same instance across renders — safe for Client Components
const client = getClientStrontiumClient({
baseURL: process.env.NEXT_PUBLIC_API_URL!,
});📚 Common Patterns
1. Server Component Data Fetch
import { createServerStrontiumClient } from '@periodic/strontium-next';
import { headers } from 'next/headers';
export default async function ProductPage({ params }: { params: { id: string } }) {
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: new Request('https://placeholder', {
headers: Object.fromEntries(headers().entries()),
}),
});
const res = await client.request<Product>({ method: 'GET', url: `/products/${params.id}` });
return <ProductDetail product={res.data} />;
}2. Mutation Route Handler with Cache Invalidation
// app/api/users/route.ts
import { createServerStrontiumClient, wrapRouteHandler, strontiumRevalidateTag } from '@periodic/strontium-next';
export const POST = wrapRouteHandler(async (req: NextRequest) => {
const client = createServerStrontiumClient({ baseURL: process.env.API_BASE_URL!, request: req });
const body = await req.json();
const res = await client.request<User>({ method: 'POST', url: '/users', body });
await strontiumRevalidateTag('users');
return NextResponse.json(res.data, { status: 201 });
});3. Authenticated Server Component
import { createServerStrontiumClient } from '@periodic/strontium-next';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const headersList = headers();
const token = headersList.get('authorization');
if (!token) redirect('/login');
const client = createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: new Request('https://placeholder', { headers: { authorization: token } }),
});
const res = await client.request<DashboardData>({ method: 'GET', url: '/dashboard' });
return <Dashboard data={res.data} />;
}4. Edge Middleware with Trace Propagation
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { createStrontiumMiddleware } from '@periodic/strontium-next';
const strontiumMiddleware = createStrontiumMiddleware({
idempotencyNamespace: 'myapp',
otelPropagation: true,
});
export function middleware(request: NextRequest) {
return strontiumMiddleware(request, () => Promise.resolve(NextResponse.next()));
}
export const config = { matcher: ['/api/:path*', '/dashboard/:path*'] };5. Severity-Based Error Handling in Route Handlers
import { CircuitOpenError, TimeoutError, RetryExhaustedError } from '@periodic/strontium';
import { wrapRouteHandler } from '@periodic/strontium-next';
export const GET = wrapRouteHandler(async (req: NextRequest) => {
// Errors are automatically mapped:
// CircuitOpenError → 503, TimeoutError → 504, RetryExhaustedError → 502
const res = await client.request({ method: 'GET', url: '/data' });
return NextResponse.json(res.data);
}, {
onError: (err) => {
if (err instanceof CircuitOpenError) sendToPagerDuty(err);
else if (err instanceof RetryExhaustedError) sendToSlack(err);
},
});6. ISR with Cache Tags
// app/api/products/route.ts
import { taggedRequest, strontiumRevalidateTag } from '@periodic/strontium-next';
// GET — attach cache tags for ISR
export const GET = wrapRouteHandler(async (req: NextRequest) => {
const res = await client.request<Product[]>(
taggedRequest({ method: 'GET', url: '/products' }, ['products'])
);
return NextResponse.json(res.data);
});
// POST — invalidate after mutation
export const POST = wrapRouteHandler(async (req: NextRequest) => {
const body = await req.json();
const res = await client.request<Product>({ method: 'POST', url: '/products', body });
await strontiumRevalidateTag('products');
return NextResponse.json(res.data, { status: 201 });
});7. Structured Logging Integration
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { createServerStrontiumClient } from '@periodic/strontium-next';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const client = createServerStrontiumClient({ baseURL: process.env.API_BASE_URL! });
client.use({
onAfterResponse: (ctx, res) => logger.info('http.response', { url: ctx.url, status: res.status }),
onError: (ctx, err) => logger.error('http.error', { url: ctx.url, error: err.message }),
onRetry: (ctx, err) => logger.warn('http.retry', { url: ctx.url, error: err.message }),
});8. Production Configuration
// lib/api-client.ts
import { createServerStrontiumClient } from '@periodic/strontium-next';
import { headers } from 'next/headers';
export function createApiClient(request?: Request) {
return createServerStrontiumClient({
baseURL: process.env.API_BASE_URL!,
request: request ?? new Request('https://placeholder', {
headers: Object.fromEntries(headers().entries()),
}),
timeoutMs: 8_000,
retry: {
enabled: true,
maxAttempts: 3,
strategy: 'exponential',
baseDelayMs: 100,
maxDelayMs: 5_000,
jitter: true,
retryOn: ['network', '5xx'],
},
circuitBreaker: {
failureThreshold: 5,
resetTimeoutMs: 30_000,
},
});
}🎛️ Configuration Options
createServerStrontiumClient Options
| Option | Type | Description |
|--------|------|-------------|
| baseURL | string | Base URL for all requests |
| request | Request \| NextRequest | Incoming request for automatic header forwarding |
| ...StrontiumConfig | — | All core createStrontiumClient options supported |
Automatically forwarded headers: x-request-id, x-correlation-id, traceparent, tracestate, authorization
wrapRouteHandler Options
| Option | Type | Description |
|--------|------|-------------|
| onError | (err: unknown) => void | Optional callback before error response is returned |
createStrontiumMiddleware Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| idempotencyNamespace | string | 'strontium' | Prefix for generated idempotency keys |
| otelPropagation | boolean | false | Propagate OpenTelemetry trace context |
📋 API Reference
Server
createServerStrontiumClient(options): StrontiumClient
createEdgeStrontiumClient(options): StrontiumClient // from '@periodic/strontium-next/edge'
getClientStrontiumClient(options): StrontiumClientRoute Handlers
wrapRouteHandler(handler, options?): RouteHandlerMiddleware
createStrontiumMiddleware(options?): NextMiddlewareCache
taggedRequest(request: RequestOptions, tags: string[]): RequestOptions
strontiumRevalidateTag(tag: string): Promise<void>
strontiumRevalidateTags(tags: string[]): Promise<void>Types
import type {
ServerClientOptions,
EdgeClientOptions,
RouteHandlerOptions,
MiddlewareOptions,
} from '@periodic/strontium-next';🧩 Architecture
@periodic/strontium-next/
├── src/
│ ├── server/ # Server-side client
│ │ ├── client.ts # createServerStrontiumClient()
│ │ └── headers.ts # Automatic header forwarding logic
│ ├── edge/ # Edge runtime client
│ │ └── client.ts # createEdgeStrontiumClient()
│ ├── browser/ # Browser-side singleton
│ │ └── client.ts # getClientStrontiumClient()
│ ├── middleware/ # Next.js middleware integration
│ │ └── index.ts # createStrontiumMiddleware()
│ ├── handlers/ # Route handler utilities
│ │ ├── wrap.ts # wrapRouteHandler()
│ │ └── errors.ts # Strontium error → HTTP status mapping
│ ├── cache/ # Next.js ISR cache integration
│ │ └── index.ts # taggedRequest, strontiumRevalidateTag
│ ├── types.ts # TypeScript interfaces
│ └── index.ts # Public APIDesign Philosophy:
- Server client forwards headers automatically — no manual plumbing
wrapRouteHandleris the error boundary — errors never escape as raw 500s- Edge client uses only Web APIs — no Node.js dependencies
- Cache utilities are thin wrappers around Next.js ISR primitives
- No opinions on your app structure — works with any folder layout
📈 Performance
- Automatic header forwarding happens at client creation time — zero overhead per request
wrapRouteHandleradds a single try/catch — negligible overhead- Edge client uses only native Web APIs — no polyfills, no bundle overhead
- Hydration-safe singleton —
getClientStrontiumClientcreates one instance, not one per render - No monkey-patching — clean wrapping only, no prototype mutation
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Its own HTTP client (use @periodic/strontium)
❌ React hooks (use @periodic/strontium-react)
❌ A cache layer beyond ISR tag support
❌ Authentication helpers — forward your own headers
❌ Automatic pagination or cursor handling
❌ Magic or implicit behavior on import
❌ Configuration files (configure in code)
Focus on doing one thing well: idiomatic Next.js integration for @periodic/strontium.
🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
ServerClientOptions,
EdgeClientOptions,
RouteHandlerOptions,
} from '@periodic/strontium-next';
// createServerStrontiumClient is fully typed
const client = createServerStrontiumClient({ baseURL: '...' });
// wrapRouteHandler preserves the handler's type signature
export const GET = wrapRouteHandler(async (req: NextRequest) => {
const res = await client.request<User[]>({ method: 'GET', url: '/users' });
return NextResponse.json(res.data); // res.data typed as User[]
});🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/strontium - The core HTTP client (required peer dependency)
- @periodic/strontium-react - React hooks integration
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/vanadium - Idempotency and distributed locks
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Environment Variables
API_BASE_URL=https://api.example.com
NEXT_PUBLIC_API_URL=https://api.example.com
NODE_ENV=productionLog Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
const client = createServerStrontiumClient({ baseURL: process.env.API_BASE_URL! });
client.use({
onAfterResponse: (ctx, res) => logger.info('http.response', { url: ctx.url, status: res.status }),
onError: (ctx, err) => logger.error('http.error', { url: ctx.url, error: err.message }),
onRetry: (ctx, err) => logger.warn('http.retry', { url: ctx.url, error: err.message }),
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Error Monitoring
const client = createServerStrontiumClient({ baseURL: process.env.API_BASE_URL! });
client.use({
onError: (ctx, err) => {
Sentry.captureException(err, { extra: { url: ctx.url } });
},
});📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
