Beyond 200 OK: Full-Stack Observability for Developers
200 OK isn’t success; we should own the complete user journey from click to memory with traceable, observable, full-stack systems.
Join the DZone community and get the full member experience.
Join For FreeYou may remember coming out of a feature meeting and saying to yourself, "My React frontend is working fine, the API goes 200 OK, I am done!"
Then, a few days later, we get a user complaint: "It's slow. Sometimes I get errors."
We then open our developer tools, look at the network tab, and maybe even backend logs. We find ourselves going down a rabbit hole, looking at multiple microservices, disconnected logs, and mysteriously high latency.
The fact of the matter is that API success is not user success when dealing with microservices, and we need to leave the frontend and API behind. Let's discuss true full-stack observability or how to learn how to trace, log, and own every single thing that happens after the fetch.
Setting Up Your TypeScript Environment
Before we get started with the examples, let's get our full-stack TypeScript environment established. You will want the following
- Frontend: React (Next.js or Vite) + Axios
- Backend: Node.js with Express (or Nest.js)
- Logging: Pino or Winston (backend), Sentry or Datadog (frontend logging)
- Tracing: OpenTelemetry for distributed tracing
- Dashboarding: Grafana, Prometheus, Datadog
Let's make sure our TypeScript tsconfig.json is strict and shared across the repo (via a base config if you are in a monorepo).
Here is how we can wire this up
Propagating Trace IDs: Connecting Frontend and Backend Logs
Trace ID enables us to track a user's click across 5 services
Step:
- Frontend: For every user click, either generate a trace ID or grab one.
- Send that trace ID in each request in the headers (for example, x-trace-id).
- Backend: The middleware pulls the trace ID and attaches it to the logs.
We should send the trace ID to all downstream services, including the database queries and queues.
Example: Axios Interceptor in React
import axios from "axios";
import { v4 as uuidv4 } from "uuid";
const traceId = uuidv4(); // or get from the session
const api = axios.create();
api.interceptors.request.use((config) => {
config.headers["x-trace-id"] = traceId;
return config;
});
Express Middleware
app.use((req, res, next) => {
const traceId = req.headers["x-trace-id"] || uuidv4();
req.traceId = traceId;
logger.info({ traceId, path: req.path }, "Incoming request");
next();
});
Now, our logs tell a story from the frontend to the backend
Logging With Context
"Just throw a console.log() in there" -- every dev, ever.
Sure, in development, yes. But what should we do in production?
Let's try the following:
- Frontend: Use Sentry with a context attached to breadcrumbs.
- Backend: Use Pino or Winston for logging in JSON format.
- Attach metadata like userId, featureFlag, traceId, and routeName.
Example: Backend Logging in Pino
import pino from 'pino';
const logger = pino({
transport: {
target: 'pino-pretty',
},
});
logger.info({
msg: "User updated profile",
userId: req.user.id,
traceId: req.traceId,
route: req.path,
});
Example Frontend Logging With Sentry
Sentry.addBreadcrumb({
category: "video-load",
message: "User clicked play",
data: { videoId: "abc123" },
level: "info",
});
With structured logs, we can now filter logs based on a route, trace id, or user, to know exactly what happened (when, where, why).
Real-World Use Case: Debugging a Latency Spike
Now, let's walk through a real bug I experienced
The user reports to you: "The video feed is taking forever to load."
Here’s how I debugged it:
- Find the Sentry error in the React frontend.
- Capture the x-trace-id from headers.
- Use the trace-id to find the backend logs.
- Found that service A was fast and service B (video metadata) was 4 sec long!
- Dived into B -> caught that Redis cache silently failed!
- Fix 1: Reconnect to cache and add retry code.
What might have taken hours to fix now takes less than 30 minutes because we can trace across the entire stack.
Dashboarding That Matter
Most dashboards are graveyards - we can change that.
We should make sure to measure only what will ultimately impact our users, like:
- API latency per route
- Frontend error rates
- Availability of third-party services
Examples of queries:
Prometheus for API latency:
Datadog RUM (frontend error boundary):
<ErrorBoundary onError={(error, info) => {
datadogRum.addError(error, {
context: { componentStack: info.componentStack },
});
}}>
<VideoFeed />
</ErrorBoundary>
Build a dashboard that answers: Is my feature working for my immediate users at this moment as I expected it to?
Distributed Tracing With OpenTelemetry
Think of OpenTelemetry as the Google Maps for your app - you can watch the malfunctions in real time.
Let's talk about the setup:
- Instrument HTTP requests (in frontend and backend)
- Wrap slow functions in spans
- Export to Jaeger or Datadog APM
Example Span for express
const span = tracer.startSpan("fetch-video-metadata");
await fetchVideoMetadata();
span.end();
Now that we are measuring latency and function execution time, rather than always wondering "where did it break?" we can actually see it.
Advanced Tips and FAQs
Q. What If I'm Using GraphQL?
The same things apply! Place the trace ID in your extensions (or in the header) and ensure that you log with context in your resolvers.
Q. Should I Log Everything?
Of course not. Log smart. Log:
- State transitions (e.g., payment started → success)
- Unexpected failures (not 2xx or a thrown error)
- Required user actions (e.g., clicked "checkout", submitted form)
Q. What Tools Are Worth It?
- Local Dev: Pino + pretty transport
- Prod: Datadog, Sentry, Grafana
- Tracing: OpenTelemetry + Jaeger
Use tools that give you visibility before your users are screaming.
Conclusion: Why This Will Make You a Better Engineer
In a distributed world, being a "full-stack developer" is much more than building a React frontend and a Node backend. It may also mean:
- Owning the journey between the user taking an action and your backend API responding
- Understanding the impact on your services
- Debugging with certainty instead of blindly guessing
Thinking this way has the added value of shortening your feedback loop while building empathy between teams and, more importantly, improving the experience for your users.
What Can You Do Today?
- Add a trace ID to every request from your frontend
- Improve your logging structure with metadata such as the service name
- Shadow your backend logs for a week--you will learn a lot
- Set up at least one meaningful dashboard. Only one.
Start owning your stack. Not just your API.
Opinions expressed by DZone contributors are their own.
Comments