Next.js Integration
Due to Next.js's full-stack architecture, we need different approaches for the client and server sides to maintain security.
The Challenge
Next.js applications run code in two environments:
- Client-side (Browser): React components, hooks, and client-side logic
- Server-side (Node.js): API routes, server components, and server actions
The problem: Using a single SDK would risk exposing your API secret key in client-side code, which is a critical security vulnerability.
The solution: Use different SDKs for each environment:
- React SDK for client-side logging (no API key required)
- Node.js SDK for server-side logging (with API key)
Installation
Install both SDKs:
npm install github:devatlogstyx/logstyx-js-react#release
npm install github:devatlogstyx/logstyx-js-node#releaseEnvironment Setup
Create a .env.local file in your project root:
# Public variables (exposed to browser)
NEXT_PUBLIC_LOGSTYX_PROJECT_ID=proj_abc123xyz
NEXT_PUBLIC_LOGSTYX_ENDPOINT=https://logs.yourcompany.com/api/v1/logs
# Private variables (server-side only)
LOGSTYX_PROJECT_SECRET=your_api_key_hereSecurity Critical
Never use NEXT_PUBLIC_ prefix for your API secret! Variables with this prefix are exposed to the browser.
Origin Configuration
Add your domain to allowed origins in Logstyx dashboard:
- Development:
http://localhost:3000 - Production:
https://yourdomain.com
Go to: Project Settings → Security → Allowed Origins
Client-Side Logging
Use the React SDK for all client-side components and interactions.
App Router (app/)
// app/providers.jsx
'use client';
import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';
const LoggerContext = createContext(null);
export function LoggerProvider({ children }) {
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
captureUncaught: true,
captureUnhandledRejections: true
});
const { ErrorBoundary } = logger;
return (
<LoggerContext.Provider value={logger}>
<ErrorBoundary logger={logger}>
{children}
</ErrorBoundary>
</LoggerContext.Provider>
);
}
export function useLogger() {
const logger = useContext(LoggerContext);
if (!logger) {
throw new Error('useLogger must be used within LoggerProvider');
}
return logger;
}// app/layout.jsx
import { LoggerProvider } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<LoggerProvider>
{children}
</LoggerProvider>
</body>
</html>
);
}// app/page.jsx
'use client';
import { useLogger } from './providers';
export default function HomePage() {
const logger = useLogger();
const { Trackable } = logger;
const handleAction = async () => {
logger.info({
message: 'Button clicked',
page: 'home'
});
};
return (
<div>
<h1>Welcome</h1>
<Trackable
event="click"
context={{ page: 'home' }}
data={{ action: 'cta-click' }}>
<button>Get Started</button>
</Trackable>
<button onClick={handleAction}>
Manual Log
</button>
</div>
);
}Pages Router (pages/)
// pages/_app.jsx
import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';
const LoggerContext = createContext(null);
export function useLogger() {
const logger = useContext(LoggerContext);
if (!logger) {
throw new Error('useLogger must be used within LoggerProvider');
}
return logger;
}
function MyApp({ Component, pageProps }) {
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
captureUncaught: true,
captureUnhandledRejections: true
});
const { ErrorBoundary } = logger;
return (
<LoggerContext.Provider value={logger}>
<ErrorBoundary logger={logger}>
<Component {...pageProps} />
</ErrorBoundary>
</LoggerContext.Provider>
);
}
export default MyApp;// pages/index.jsx
import { useLogger } from './_app';
export default function HomePage() {
const logger = useLogger();
const { Trackable } = logger;
return (
<div>
<Trackable event="click" data={{ action: 'signup' }}>
<button>Sign Up</button>
</Trackable>
</div>
);
}Server-Side Logging
Use the Node.js SDK for API routes, server components, and server actions.
Setting Up the Logger
Create a shared logger instance for server-side code:
// lib/server-logger.js
import useLogstyx from 'logstyx-js-node';
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
apiKey: process.env.LOGSTYX_PROJECT_SECRET,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
captureUncaught: true,
captureUnhandledRejections: true
});
// Set application-wide context
logger.setContext({
service: 'NextJS App',
environment: process.env.NODE_ENV
});
export default logger;API Route Handler (Recommended Pattern)
Create a wrapper to automatically log all API requests:
// lib/rest-handler.js
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import logger from './server-logger';
const SUCCESS_LOG_LEVEL = 'info';
const ERROR_LOG_LEVEL = 'error';
// Redact sensitive fields from objects
function redactObject(obj) {
if (!obj || typeof obj !== 'object') return obj;
const redacted = { ...obj };
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
for (const key of Object.keys(redacted)) {
if (sensitiveFields.some(field =>
key.toLowerCase().includes(field.toLowerCase())
)) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}
export function RestHandler(handler) {
return async (request, context) => {
const { searchParams, pathname } = new URL(request.url);
const cookieStore = cookies();
let body = null;
let query = Object.fromEntries(searchParams.entries());
// Parse body for non-GET requests
if (request.method !== 'GET' && request.method !== 'HEAD') {
try {
const clonedRequest = request.clone();
body = await clonedRequest.json().catch(() => null);
} catch {
body = null;
}
}
try {
// Call the actual handler
const response = await handler(request, context);
// Clone and read response (safely handle non-JSON responses)
const clonedResponse = response.clone();
let responseData = null;
try {
responseData = await clonedResponse.json();
} catch {
// Response is not JSON, that's okay
responseData = { contentType: response.headers.get('content-type') };
}
// Only log non-GET requests to reduce noise
if (request.method !== 'GET' && request.method !== 'HEAD') {
logger.send(SUCCESS_LOG_LEVEL, {
title: `${request.method} ${pathname}`,
message: 'Request completed successfully',
context: {
query: redactObject(query),
body: redactObject(body),
cookies: cookieStore.getAll(),
response: responseData,
statusCode: response.status
}
}).catch(console.error);
}
return response;
} catch (error) {
// Log error with full context
logger.send(ERROR_LOG_LEVEL, {
title: `${request.method} ${pathname}`,
message: error?.message || 'Unknown error',
context: {
stack: error?.stack,
query: redactObject(query),
body: redactObject(body),
cookies: cookieStore.getAll()
}
}).catch(console.error);
return NextResponse.json(
{
error: error?.message || 'Internal server error',
code: 'SERVER_ERROR'
},
{ status: error?.response?.status || 500 }
);
}
};
}
export { logger };Using RestHandler in API Routes
App Router
// app/api/users/route.js
import { NextResponse } from 'next/server';
import { RestHandler } from '@/lib/rest-handler';
export const GET = RestHandler(async (request, { params }) => {
// Your business logic here
const users = await fetchUsers();
return NextResponse.json({
success: true,
data: users
});
});
export const POST = RestHandler(async (request, { params }) => {
const body = await request.json();
// Your business logic
const newUser = await createUser(body);
return NextResponse.json({
success: true,
data: newUser
});
});Pages Router
For Pages Router, create a separate handler since it uses a different API pattern:
// lib/pages-rest-handler.js
import logger from './server-logger';
const SUCCESS_LOG_LEVEL = 'info';
const ERROR_LOG_LEVEL = 'error';
function redactObject(obj) {
if (!obj || typeof obj !== 'object') return obj;
const redacted = { ...obj };
const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'creditCard'];
for (const key of Object.keys(redacted)) {
if (sensitiveFields.some(field =>
key.toLowerCase().includes(field.toLowerCase())
)) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}
export function PagesRestHandler(handler) {
return async (req, res) => {
const startTime = Date.now();
try {
// Call the actual handler
await handler(req, res);
// Only log non-GET requests
if (req.method !== 'GET' && req.method !== 'HEAD') {
logger.send(SUCCESS_LOG_LEVEL, {
title: `${req.method} ${req.url}`,
message: 'Request completed successfully',
context: {
query: redactObject(req.query),
body: redactObject(req.body),
statusCode: res.statusCode,
duration: Date.now() - startTime
}
}).catch(console.error);
}
} catch (error) {
logger.send(ERROR_LOG_LEVEL, {
title: `${req.method} ${req.url}`,
message: error?.message || 'Unknown error',
context: {
stack: error?.stack,
query: redactObject(req.query),
body: redactObject(req.body),
duration: Date.now() - startTime
}
}).catch(console.error);
// Re-throw to let Next.js handle it
if (!res.headersSent) {
res.status(500).json({
error: error?.message || 'Internal server error',
code: 'SERVER_ERROR'
});
}
}
};
}
export { logger };// pages/api/users/index.js
import { PagesRestHandler } from '@/lib/pages-rest-handler';
async function handler(req, res) {
if (req.method === 'GET') {
const users = await fetchUsers();
res.status(200).json({ success: true, data: users });
} else if (req.method === 'POST') {
const newUser = await createUser(req.body);
res.status(201).json({ success: true, data: newUser });
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
export default PagesRestHandler(handler);Server Actions
// app/actions/user-actions.js
'use server';
import logger from '@/lib/server-logger';
export async function updateUserProfile(formData) {
try {
logger.info({
message: 'Updating user profile',
userId: formData.get('userId')
});
const result = await saveUserProfile(formData);
logger.info({
message: 'Profile updated successfully',
userId: formData.get('userId')
});
return { success: true, data: result };
} catch (error) {
logger.error({
message: 'Profile update failed',
error: error.message,
userId: formData.get('userId')
});
return { success: false, error: error.message };
}
}Server Components
// app/dashboard/page.jsx
import logger from '@/lib/server-logger';
export default async function DashboardPage() {
try {
logger.info({ message: 'Loading dashboard' });
const data = await fetchDashboardData();
return (
<div>
<h1>Dashboard</h1>
{/* Render data */}
</div>
);
} catch (error) {
logger.error({
message: 'Dashboard load failed',
error: error.message
});
throw error;
}
}Complete Example
Here's a full example showing both client and server-side logging:
Directory Structure
my-nextjs-app/
├── app/
│ ├── api/
│ │ └── users/
│ │ └── route.js # API route with logging
│ ├── actions/
│ │ └── user-actions.js # Server actions
│ ├── layout.jsx # Root layout with provider
│ ├── page.jsx # Home page
│ └── providers.jsx # Client logger provider
├── lib/
│ ├── server-logger.js # Server-side logger
│ ├── rest-handler.js # API handler wrapper (App Router)
│ └── pages-rest-handler.js # API handler wrapper (Pages Router)
└── .env.local # Environment variablesImplementation
// lib/server-logger.js
import useLogstyx from 'logstyx-js-node';
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
apiKey: process.env.LOGSTYX_PROJECT_SECRET,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
captureUncaught: process.env.NODE_ENV === 'production',
captureUnhandledRejections: process.env.NODE_ENV === 'production'
});
logger.setContext({
service: 'E-commerce Platform',
environment: process.env.NODE_ENV,
version: '1.0.0'
});
export default logger;// app/providers.jsx
'use client';
import { createContext, useContext } from 'react';
import useLogstyx from 'logstyx-js-react';
const LoggerContext = createContext(null);
export function LoggerProvider({ children }) {
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
captureUncaught: true,
captureUnhandledRejections: true
});
const { ErrorBoundary } = logger;
return (
<LoggerContext.Provider value={logger}>
<ErrorBoundary logger={logger}>
{children}
</ErrorBoundary>
</LoggerContext.Provider>
);
}
export function useLogger() {
const logger = useContext(LoggerContext);
if (!logger) {
throw new Error('useLogger must be used within LoggerProvider');
}
return logger;
}// app/page.jsx
'use client';
import { useLogger } from './providers';
export default function HomePage() {
const logger = useLogger();
const { Trackable } = logger;
const handleCheckout = async () => {
logger.info({
message: 'Checkout initiated',
page: 'home'
});
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [...] })
});
const data = await response.json();
logger.info({
message: 'Checkout completed',
orderId: data.orderId
});
} catch (error) {
logger.error({
message: 'Checkout failed',
error: error.message
});
}
};
return (
<div>
<h1>Shop Now</h1>
<Trackable
event="click"
context={{ page: 'home', section: 'hero' }}
data={{ action: 'view-products' }}>
<button>Browse Products</button>
</Trackable>
<button onClick={handleCheckout}>
Checkout
</button>
</div>
);
}// app/api/checkout/route.js
import { NextResponse } from 'next/server';
import { RestHandler } from '@/lib/rest-handler';
export const POST = RestHandler(async (request) => {
const body = await request.json();
// Process checkout
const order = await processCheckout(body.items);
return NextResponse.json({
success: true,
orderId: order.id,
total: order.total
});
});Best Practices
1. Never Expose API Secrets
// ❌ WRONG - Exposes secret to browser
const logger = useLogstyx({
apiKey: process.env.LOGSTYX_PROJECT_SECRET // NEVER do this in client code!
});
// ✅ CORRECT - Client-side (no API key)
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT
});
// ✅ CORRECT - Server-side only
import logger from '@/lib/server-logger'; // Safe, runs server-side only2. Use Environment-Specific Configuration
// lib/server-logger.js
const logger = useLogstyx({
projectId: process.env.NEXT_PUBLIC_LOGSTYX_PROJECT_ID,
apiKey: process.env.LOGSTYX_PROJECT_SECRET,
endpoint: process.env.NEXT_PUBLIC_LOGSTYX_ENDPOINT,
// Only capture errors in production
captureUncaught: process.env.NODE_ENV === 'production',
captureUnhandledRejections: process.env.NODE_ENV === 'production'
});3. Redact Sensitive Data
Always redact passwords, tokens, and PII:
function redactObject(obj) {
if (!obj || typeof obj !== 'object') return obj;
const redacted = { ...obj };
const sensitiveFields = [
'password',
'token',
'secret',
'apiKey',
'creditCard',
'ssn'
];
for (const key of Object.keys(redacted)) {
if (sensitiveFields.some(field =>
key.toLowerCase().includes(field.toLowerCase())
)) {
redacted[key] = '[REDACTED]';
}
}
return redacted;
}4. Set Application Context
Provide helpful context at the application level:
// lib/server-logger.js
logger.setContext({
service: 'API Server',
environment: process.env.NODE_ENV,
version: process.env.npm_package_version,
region: process.env.VERCEL_REGION
});5. Structure Your Logs
Use consistent structure for better searchability:
// ❌ Avoid
logger.info('User logged in');
// ✅ Better
logger.info({
message: 'User logged in',
userId: user.id,
method: 'oauth',
timestamp: Date.now()
});6. Log Request Context in API Routes
Include useful request information:
logger.info({
message: 'API request',
method: request.method,
path: pathname,
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for')
});Troubleshooting
API Key Exposed Error
Issue: Getting security warnings about exposed API keys
Solution: Ensure you're only using the API key in server-side code:
- Server components (no 'use client' directive)
- API routes
- Server actions
- lib/server-logger.js
Never import logstyx-js-node in client components.
CORS Errors
Issue: Getting CORS errors when logging from browser
Solution: Add your domains to allowed origins in Logstyx dashboard:
- Go to Project Detail → Settings
- Add origins:
http://localhost:3000(development)https://yourdomain.com(production)
Logs Not Appearing
Issue: Logs aren't showing up in dashboard
Solution: Check that:
- Environment variables are set correctly
NEXT_PUBLIC_prefix is used for client-side variables- API endpoint is correct and accessible
- Project ID matches your Logstyx project
Double Logging in Development
Issue: Seeing duplicate logs in development
Solution: This is normal in development due to React Strict Mode. It won't happen in production.
Migration from Other Logging Solutions
From console.log
// Before
console.log('User logged in:', userId);
console.error('Failed to load data:', error);
// After - Client
const logger = useLogger();
logger.info({ message: 'User logged in', userId });
logger.error({ message: 'Failed to load data', error: error.message });
// After - Server
import logger from '@/lib/server-logger';
logger.info({ message: 'User logged in', userId });
logger.error({ message: 'Failed to load data', error: error.message });From Winston/Pino
// Before
const winston = require('winston');
const logger = winston.createLogger({...});
logger.info('Request processed', { userId, duration });
// After
import logger from '@/lib/server-logger';
logger.info({
message: 'Request processed',
userId,
duration
});