<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: The Practical Developer</title>
    <description>The latest articles on DEV Community by The Practical Developer (@practicaldevblog).</description>
    <link>https://dev.to/practicaldevblog</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209076%2F96c9c756-0d00-484d-b6a5-5b57088c18e9.jpg</url>
      <title>DEV Community: The Practical Developer</title>
      <link>https://dev.to/practicaldevblog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/practicaldevblog"/>
    <language>en</language>
    <item>
      <title>Sentry Error Tracking: Catch Bugs Before Your Users Do</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Sat, 14 Nov 2026 07:19:29 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/sentry-error-tracking-catch-bugs-before-your-users-do-1j3b</link>
      <guid>https://dev.to/practicaldevblog/sentry-error-tracking-catch-bugs-before-your-users-do-1j3b</guid>
      <description>&lt;h2&gt;Quick answer&lt;/h2&gt;

&lt;p&gt;Sentry error tracking collects the exceptions your Python app raises in production and groups them into issues you can triage, with the stack trace and how many users were hit. Install sentry-sdk, call sentry_sdk.init() with your project DSN as early as possible, and every unhandled exception is reported automatically. It is worth it once real users can hit errors you never see in development.&lt;/p&gt;

&lt;p&gt;Your checkout worked on your laptop. It passed review, the tests were green, and the deploy went out on a Friday afternoon. Then a customer writes on Monday that their order failed, and you have nothing to go on. No log line, no stack trace, just a screenshot and a vague description. You open the logs and find a wall of lines, most harmless, while the one line that matters sits buried in the middle. That gap, between an error happening and your team knowing about it, is the whole problem. Sentry closes it by catching the exception the moment it happens and sending you the details.&lt;/p&gt;

&lt;h2&gt;What I would check before using it&lt;/h2&gt;

&lt;p&gt;My first check is whether real users can reach the code. If the app only runs on your own machine, a traceback in the terminal is enough and Sentry adds nothing. Once strangers can trigger a crash, you need to know about it before they tell you.&lt;/p&gt;

&lt;p&gt;My second check is error volume against the free tier. Sentry gives you a monthly event allowance, and one noisy loop raising the same exception hourly will eat it in a day. Before I would switch it on, I want alert rules that group repeats instead of paging me for each one.&lt;/p&gt;

&lt;p&gt;My third check is what data leaves my server. An event carries the stack trace, and it can also carry request data and local variables. If the app handles passwords or payment details, I want to test how Sentry scrubs them with a deliberately bad event first.&lt;/p&gt;

&lt;p&gt;My fourth check is framework support. The Python SDK plugs into Django, Flask, and FastAPI with almost no extra code. A small &lt;a href="https://practicaldevblog.blogspot.com/2026/10/flask-explained-small-framework-that.html" rel="noopener noreferrer"&gt;Flask&lt;/a&gt; service needs the same two steps as anything else: install the SDK and call init early. The same init-first rule applies on the Node side, as the &lt;a href="https://practicaldevblog.blogspot.com/2026/10/express-explained-nodes-minimal-web.html" rel="noopener noreferrer"&gt;Express&lt;/a&gt; post shows.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: adopt Sentry error tracking when the app is in production, real users can trigger errors, and someone owns the triage. Skip it for a weekend project or a script you run by hand. The recommendation flips the moment error volume outruns the budget, or sensitive data cannot be scrubbed cleanly, in which case self-hosted logging is the better trade.&lt;/p&gt;

&lt;h2&gt;One order total, run end to end&lt;/h2&gt;

&lt;p&gt;The example is a small shop checkout: three items with a name, a price in rupees, and a quantity. The order_total function adds up price times quantity. Two items are fine. The third, a laptop stand, has no price at all, because the catalogue entry was saved before anyone filled that field in. That is the kind of bad data that passes every hand-built test cart, then breaks the first real order.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import sentry_sdk

cart = [
    {"name": "Mechanical keyboard", "price": 18500, "qty": 1},
    {"name": "USB-C hub", "price": 6400, "qty": 2},
    {"name": "Laptop stand", "qty": 1},
]

def order_total(items):
    total = 0
    for item in items:
        total += item["price"] * item["qty"]
    return total

captured = []

def record(event, hint):
    captured.append(event)
    return event&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The script runs the same cart twice: once before Sentry is initialised, the mistake I wanted to show, and once after sentry_sdk.init(), with a before_send hook saving every event into a list. I used a placeholder DSN, so nothing reached a dashboard.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;print("Phase 1: crash before Sentry is initialised")
try:
    print("Total:", order_total(cart))
except KeyError as exc:
    print("KeyError:", exc)
print("Events captured so far:", len(captured))

print("Phase 2: Sentry initialised, running the same cart again")
sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
    before_send=record,
)
try:
    print("Total:", order_total(cart))
except KeyError:
    sentry_sdk.capture_exception()
    print("Caught the KeyError and handed it to Sentry.")
sentry_sdk.flush()
print("Events captured:", len(captured))
if captured:
    exc = captured[0]["exception"]["values"][0]
    print("Captured event:", exc["type"], exc["value"])&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here is the real output from the run:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Phase 1: crash before Sentry is initialised
KeyError: 'price'
Events captured so far: 0
Phase 2: Sentry initialised, running the same cart again
Caught the KeyError and handed it to Sentry.
Events captured: 1
Captured event: KeyError 'price'
Sentry is attempting to send 1 pending events
Waiting up to 2 seconds
Press Ctrl-C to quit&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The thing that went wrong is in phase 1: the cart crashed with KeyError: 'price', and zero events were captured, because Sentry had not been initialised yet. An SDK cannot report an error from before it existed. The fix is ordering, not configuration: call sentry_sdk.init() at the very top of your entry point, before any app code runs. In phase 2, the same crash produced exactly one captured event, the exception type and value printed from the list. The last three lines are the SDK flushing on exit. The data needs a fix too: adding the missing price of 9200 makes the fixed cart total 40500. Better still, validate incoming records first, so a missing field fails at the door. My post on &lt;a href="https://practicaldevblog.blogspot.com/2026/11/pydantic-catch-bad-data-before-it.html" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt; shows how that validation looks in practice.&lt;/p&gt;

&lt;h2&gt;Common mistakes and how to fix them&lt;/h2&gt;

&lt;p&gt;First, initialising Sentry too late. The symptom is silence: crashes happen, users complain, and the Sentry dashboard shows nothing. The fix is to call sentry_sdk.init() at the top of your entry point, before imports that run app code, before the server starts listening, and before any background thread starts work.&lt;/p&gt;

&lt;p&gt;Second, swallowing exceptions with a bare except. The symptom is a function that returns None or an empty page instead of crashing, so Sentry never hears about it and the bug hides for weeks. The fix is to call sentry_sdk.capture_exception() inside the handler, then re-raise or return a proper error, so the event is recorded and the caller still knows something failed.&lt;/p&gt;

&lt;p&gt;Third, turning on error capture but never setting traces_sample_rate. The symptom is that errors arrive fine, but the performance pages stay empty and slow checkout calls have no trace to inspect. The fix is to set traces_sample_rate to a small value such as 0.1 in production, so a sample of requests produces traces without flooding your quota.&lt;/p&gt;

&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Sentry free to start with?&lt;/strong&gt; Yes, with a monthly event allowance that suits a small app. Check the pricing page first, because a noisy error loop can burn through it fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Sentry slow down my Python app?&lt;/strong&gt; Barely. The SDK sends events in the background, and the cost per request is small. Capture every request with tracing, though, and you will feel it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I test capture without a real Sentry account?&lt;/strong&gt; Yes. This post does exactly that, with a before_send hook saving events into a list and a placeholder DSN. A real project DSN sends events to the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Sentry work with Django, Flask, and FastAPI?&lt;/strong&gt; Yes, the Python SDK ships integrations for all three, attaching automatically once initialised. The same idea applies beyond Python: a &lt;a href="https://practicaldevblog.blogspot.com/2026/10/javascript-explained-language-that-runs.html" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; frontend uses the JavaScript SDK the same way.&lt;/p&gt;

&lt;h2&gt;What to remember&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Call sentry_sdk.init() first, before any code that can crash, or early errors are never reported.&lt;/li&gt;
&lt;li&gt;Assign an owner for the issues inbox, or captured events pile up unread.&lt;/li&gt;
&lt;li&gt;Validate incoming data so a missing field fails at the door, not at checkout.&lt;/li&gt;
&lt;li&gt;Sample traces and set alert rules, or a noisy loop eats your quota.&lt;/li&gt;
&lt;li&gt;Test capture locally with before_send before trusting production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Where this leaves you&lt;/h2&gt;

&lt;p&gt;Sentry error tracking earns its place the day strangers can break your app in ways you never tested. The demo proved the two things that matter: an uninitialised SDK captures nothing, and the same crash after init produces a real event with the exception type and the missing key attached. That is the whole job. Start with the free tier, wire init into your entry point, and check the issues page after your next deploy. If the inbox stays quiet, you have peace of mind. If it fills up, you have a list of real bugs, which is the better outcome.&lt;/p&gt;

&lt;h2&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sentry for Python, &lt;a href="https://docs.sentry.io/platforms/python/" rel="noopener noreferrer"&gt;https://docs.sentry.io/platforms/python/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sentry Issues, error monitoring product docs, &lt;a href="https://docs.sentry.io/product/error-monitoring/" rel="noopener noreferrer"&gt;https://docs.sentry.io/product/error-monitoring/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/10/flask-explained-small-framework-that.html" rel="noopener noreferrer"&gt;Flask Explained: The Small Framework That Does Just Enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/10/javascript-explained-language-that-runs.html" rel="noopener noreferrer"&gt;JavaScript Explained: The Language That Runs the Whole Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/11/pydantic-catch-bad-data-before-it.html" rel="noopener noreferrer"&gt;Pydantic: Catch Bad Data Before It Reaches Your Database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Waqas Shahid is a software engineer from Karachi with more than 12 years of experience, including 6+ years in engineering leadership. He writes The Practical Developer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/sentry-error-tracking-python.html" rel="noopener noreferrer"&gt;The Practical Developer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sentry</category>
      <category>python</category>
      <category>monitoring</category>
      <category>debugging</category>
    </item>
    <item>
      <title>OpenTelemetry Python: Trace a Checkout Flow End to End</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Sat, 14 Nov 2026 07:18:10 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/opentelemetry-python-trace-a-checkout-flow-end-to-end-2bol</link>
      <guid>https://dev.to/practicaldevblog/opentelemetry-python-trace-a-checkout-flow-end-to-end-2bol</guid>
      <description>&lt;h2&gt;Quick answer&lt;/h2&gt;

&lt;p&gt;Yes. OpenTelemetry Python adds distributed tracing to a Python service with a few lines of setup: create a tracer provider, attach an exporter, and wrap each step of a request in a span. Each span records a name, timing, and attributes, and spans from one request share a trace id. Start with the console exporter, then switch to a real backend when the traces look right.&lt;/p&gt;

&lt;p&gt;A customer writes in: checkout took thirty seconds, then failed, and nobody can say where the time went. The order service says the payment call was slow. The payment service says it never saw the request. The logs from both sides exist, but they disagree about what happened first. That is the gap tracing fills. A trace follows one request across every service it touches, giving you one timeline with a name, a duration, and a few facts per step. OpenTelemetry is the shared vocabulary for that timeline, and the Python SDK is the part you wire into your own code.&lt;/p&gt;

&lt;h2&gt;What I would check before using it&lt;/h2&gt;

&lt;p&gt;My first check is whether a request crosses a boundary. A single small script gains almost nothing, because a log line already tells you what happened. The payoff starts when a request crosses an HTTP call, a queue, or a database, and you need to know which side was slow.&lt;/p&gt;

&lt;p&gt;My second check is who will read the traces. Spans in a terminal are for learning, not operations. In production you need a backend that stores and searches traces, which means running or paying for one. If nobody opens that dashboard during an incident, the instrumentation is decoration.&lt;/p&gt;

&lt;p&gt;My third check is how the code is structured. Manual spans work well when each step is a clear function, like the checkout below. If the work is spread across callbacks or threads, keeping the context attached takes real care. A framework with built-in instrumentation, such as &lt;a href="https://practicaldevblog.blogspot.com/2026/10/fastapi-explained-python-apis-at-full.html" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt;, removes most of that friction, so look for an integration first.&lt;/p&gt;

&lt;p&gt;My fourth check is cost. Every span is data, and a busy service can produce millions a day. Sampling, keeping one trace in ten, keeps the bill sane, but the one failing request you care about may be the one dropped. Make that trade deliberately.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: adopt it when a request crosses at least one service boundary and someone will read the traces during an incident. Start with the console exporter on one flow, and switch only when the spans answer a real question. Skip it for a single-process app. The recommendation flips the moment a second service appears, or debugging by log timestamps starts eating afternoons.&lt;/p&gt;

&lt;h2&gt;One checkout, traced end to end&lt;/h2&gt;

&lt;p&gt;The example is a small checkout: validate the order, charge the payment, confirm the order. Three sample orders run through it, each with an id, a customer, a total, and an item count. Each step runs inside a span, and the whole checkout runs inside a parent span, so all four spans for one order share a trace id.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

resource = Resource(attributes={"service.name": "checkout-service"})
provider = TracerProvider(resource=resource)
trace.set_tracer_provider(provider)
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
tracer = trace.get_tracer("checkout.tracer")

ORDERS = [
    {"order_id": "ORD-2101", "customer": "Ayesha Khan", "total": 12400, "items": 2},
    {"order_id": "ORD-2102", "customer": "Bilal Ahmed", "total": 7800, "items": 1},
    {"order_id": "ORD-2103", "customer": "Sana Malik", "total": 21950, "items": 4},
]


def validate_order(order):
    with tracer.start_as_current_span("validate_order") as span:
        span.set_attribute("order.id", order["order_id"])
        span.set_attribute("order.items", order["items"])
        if order["total"] &amp;lt;= 0:
            raise ValueError("order total must be positive")
        return True


def charge_payment(order):
    with tracer.start_as_current_span("charge_payment") as span:
        span.set_attribute("order.id", order["order_id"])
        span.set_attribute("payment.amount_pkr", order["total"])
        return f"PAY-{order['order_id'][-4:]}"


def confirm_order(order, payment_ref):
    with tracer.start_as_current_span("confirm_order") as span:
        span.set_attribute("order.id", order["order_id"])
        span.set_attribute("payment.ref", payment_ref)
        return f"Confirmed {order['order_id']} for {order['customer']}"


def checkout(order):
    with tracer.start_as_current_span("checkout") as span:
        span.set_attribute("order.id", order["order_id"])
        validate_order(order)
        payment_ref = charge_payment(order)
        result = confirm_order(order, payment_ref)
        print(result)


for order in ORDERS:
    checkout(order)

print("All checkouts done.")&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first run went wrong in the quietest possible way. I had written the provider and the tracer, but not added the span processor, so the spans were created and dropped. The program printed the three confirmation lines, exited 0, and printed no spans at all:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Confirmed ORD-2101 for Ayesha Khan
Confirmed ORD-2102 for Bilal Ahmed
Confirmed ORD-2103 for Sana Malik
All checkouts done.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nothing failed, nothing warned, and that is why this mistake is so common. The fix is one line, added before the tracer is fetched: &lt;code&gt;provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))&lt;/code&gt;. With it in place, the same run printed 12 spans, four per order. Here is one span from the real output (ids and timestamps differ on every run):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "name": "validate_order",
    "context": {
        "trace_id": "0x8ddfe446f4041fce1b4f38234176697c",
        "span_id": "0x502253268eda2d75",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x46e7760eea1ddbef",
    "start_time": "2026-11-14T04:06:39.929369Z",
    "end_time": "2026-11-14T04:06:39.929401Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "order.id": "ORD-2101",
        "order.items": 2
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "service.name": "checkout-service"
        },
        "schema_url": ""
    }
}
Confirmed ORD-2101 for Ayesha Khan
Confirmed ORD-2102 for Bilal Ahmed
Confirmed ORD-2103 for Sana Malik
All checkouts done.&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The useful detail is the parent id. The validate_order span points at the checkout span as its parent, and the checkout span has a null parent id, marking it as the root. That parent link is the whole mechanism: it turns four separate records into one timeline. Spans also print in the order they finish, so children appear before their parent. The same habit pays off once the flow grows past one process, whether services run in &lt;a href="https://practicaldevblog.blogspot.com/2026/10/docker-explained-containers-without.html" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; containers, talk through &lt;a href="https://practicaldevblog.blogspot.com/2026/10/kafka-explained-event-backbone-of-big.html" rel="noopener noreferrer"&gt;Kafka&lt;/a&gt; events, or are orchestrated by &lt;a href="https://practicaldevblog.blogspot.com/2026/10/kubernetes-explained-running-containers.html" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;Common mistakes and how to fix them&lt;/h2&gt;

&lt;p&gt;First, creating spans but never adding a span processor. The symptom is silence: the program runs fine, exits 0, and no spans appear. The fix is to add the processor before creating spans, and run once with the console exporter.&lt;/p&gt;

&lt;p&gt;Second, fetching the tracer before setting the provider, or setting it twice. The symptom is a warning about overriding the global provider, or spans landing somewhere unconfigured. The fix is to set the provider once, at the top of your entry point, then fetch the tracer.&lt;/p&gt;

&lt;p&gt;Third, high-cardinality values in span attributes, like a full email or a URL carrying a session token. The symptom is a backend that grows fast, costs more, and may store data you should never have logged. The fix is to keep attributes small, an order id, a status, a count.&lt;/p&gt;

&lt;p&gt;Fourth, forgetting that context does not cross threads on its own. The symptom is child spans appearing as separate root traces instead of children of the request. The fix is to pass the context into the thread, or use the framework integration that does it for you.&lt;/p&gt;

&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is distributed tracing?&lt;/strong&gt; It follows one request across every service it touches, recording each step as a span. Spans from one request share a trace id, so you read one timeline, not scattered log lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Jaeger or another backend to start?&lt;/strong&gt; No. The console exporter prints spans to the terminal and needs nothing else. Move to a backend only when you want to store and share traces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is OpenTelemetry only for microservices?&lt;/strong&gt; No, but that is where it pays best. A single service still benefits when a request has several slow steps. If the whole request is one database query, plain logging is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much overhead does tracing add?&lt;/strong&gt; The spans here cost microseconds around steps that take far longer. The real cost is data volume, so plan sampling and retention first.&lt;/p&gt;

&lt;h2&gt;What to remember&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A trace is one request, made of spans sharing a trace id.&lt;/li&gt;
&lt;li&gt;Set the provider once, add a span processor, then fetch the tracer.&lt;/li&gt;
&lt;li&gt;If spans do not print to the console, no backend will save you.&lt;/li&gt;
&lt;li&gt;Keep span attributes small, and never put secrets in them.&lt;/li&gt;
&lt;li&gt;Tracing pays when a request crosses a boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Where this leaves you&lt;/h2&gt;

&lt;p&gt;You now have a working trace for a checkout flow, built from a provider, a processor, an exporter, and four spans per order, and you have seen the failure that catches most beginners: spans created, never exported, silence mistaken for success. The next step is small on purpose. Take one real flow in your own service, the one people ask about when it is slow, add spans to its steps, and run it with the console exporter. If the spans answer a question your logs could not, you have found where tracing belongs. If not, you spent an afternoon, not a quarter, finding out.&lt;/p&gt;

&lt;h2&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenTelemetry Python documentation, &lt;a href="https://opentelemetry.io/docs/languages/python/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/languages/python/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry traces concepts, &lt;a href="https://opentelemetry.io/docs/concepts/signals/traces/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/concepts/signals/traces/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry Python API and SDK reference, &lt;a href="https://opentelemetry-python.readthedocs.io/" rel="noopener noreferrer"&gt;https://opentelemetry-python.readthedocs.io/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Waqas Shahid is a software engineer from Karachi with more than 12 years of experience, including 6+ years in engineering leadership. He writes The Practical Developer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/opentelemetry-python-checkout-tracing.html" rel="noopener noreferrer"&gt;The Practical Developer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>tracing</category>
    </item>
    <item>
      <title>LangChain Tutorial: Build Your First LLM App in Python</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Sat, 14 Nov 2026 07:16:50 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/langchain-tutorial-build-your-first-llm-app-in-python-45m</link>
      <guid>https://dev.to/practicaldevblog/langchain-tutorial-build-your-first-llm-app-in-python-45m</guid>
      <description>&lt;h2&gt;Quick answer&lt;/h2&gt;

&lt;p&gt;LangChain is a Python framework for building applications powered by large language models, and in current LangChain 1.x you compose those applications with LCEL, the LangChain Expression Language, written as prompt | model | parser. This post builds a working support-ticket triage chain with that pattern, running offline with a stand-in model so you can learn the shape before spending anything on API calls.&lt;/p&gt;

&lt;p&gt;Picture a support inbox on a Monday morning. Two hundred messages arrived over the weekend: billing questions, outage reports, one customer who cannot check out. Reading each one by hand takes hours, and the urgent ones hide among the routine ones. A language model can read and sort them in seconds, but wiring that into real code means formatting prompts, calling the API, parsing replies, and handling failures. That wiring is the job LangChain is built for, and this post shows it on a small, honest example.&lt;/p&gt;

&lt;h2&gt;What I would check before using it&lt;/h2&gt;

&lt;p&gt;My first check is whether the work is really a pipeline. LangChain earns its keep when the same task runs again and again: read a ticket, classify it, route it. A single one-off call needs no framework.&lt;/p&gt;

&lt;p&gt;My second check is how fast the framework moves. LangChain has changed its recommended patterns more than once, and old tutorials still teach LLMChain and SequentialChain, both deprecated since 0.1.17 and gone from the 1.0 way of working. Read the current docs and pin your versions before you build on it.&lt;/p&gt;

&lt;p&gt;My third check is where the model lives. LangChain is not a model, it is the wiring around one. You still need an API key and a paid account for a real model, or a local model of your own. The demo uses FakeListLLM, a stand-in returning canned replies, so you can learn the wiring without an account or a bill.&lt;/p&gt;

&lt;p&gt;My fourth check is where the result goes next. A triage verdict is only useful if it lands somewhere, a queue, a database, or a chat channel. If the output stays as a printed string, you have built a demo, not a feature. A real deployment would parse it and hand it to a small web service, like the one in the &lt;a href="https://practicaldevblog.blogspot.com/2026/10/flask-explained-small-framework-that.html" rel="noopener noreferrer"&gt;Flask post on this blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use LangChain for a repeated LLM pipeline, when you accept that the API moves, and you have a real model and a real destination for the output. Skip it for one-off calls. The recommendation flips the moment a simpler direct API call covers the whole job.&lt;/p&gt;

&lt;h2&gt;One triage chain, run end to end&lt;/h2&gt;

&lt;p&gt;The example sorts four support messages for a small software company. Each ticket gets an urgency, a topic, and one short action, in one line for easy routing. The chain is three pieces joined with the pipe operator: a ChatPromptTemplate that fills in the ticket text, a model, and a StrOutputParser that turns the reply into a plain string. There is no API key on this machine, so the model is FakeListLLM, a stand-in from langchain_core that returns canned replies in order. The replies below are the ones I wrote into the stand-in, not a real model's judgement. Swapping in a real chat model later means changing one line.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;from langchain_core.language_models import FakeListLLM
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

tickets = [
    {"id": "TKT-101", "message": "I was charged twice for my March subscription and the second charge is still showing as pending."},
    {"id": "TKT-102", "message": "The dashboard shows a blank page after I log in. I have tried two browsers and cleared the cache."},
    {"id": "TKT-103", "message": "Can I change the email address on my account? I no longer use the old one."},
    {"id": "TKT-104", "message": "Our checkout has been down for 40 minutes and customers cannot pay. This is urgent."},
]

prompt = ChatPromptTemplate.from_template(
    "You triage support tickets for a small software company. "
    "Read the ticket and reply in exactly one line with this shape: "
    "URGENCY: &amp;lt;low, medium, or high&amp;gt; | TOPIC: &amp;lt;billing, technical, or account&amp;gt; "
    "| ACTION: &amp;lt;one short action, under 10 words&amp;gt;. "
    "Ticket: {message}"
)

model = FakeListLLM(
    responses=[
        "URGENCY: medium | TOPIC: billing | ACTION: Refund the duplicate March charge",
        "URGENCY: high | TOPIC: technical | ACTION: Reproduce the blank dashboard and check logs",
        "URGENCY: low | TOPIC: account | ACTION: Confirm identity, then update the email",
        "URGENCY: high | TOPIC: technical | ACTION: Page the on-call engineer for checkout",
    ]
)

chain = prompt | model | StrOutputParser()

for ticket in tickets:
    result = chain.invoke({"message": ticket["message"]})
    print(f"{ticket['id']}: {result}")&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here is the real output from the run:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;TKT-101: URGENCY: medium | TOPIC: billing | ACTION: Refund the duplicate March charge
TKT-102: URGENCY: high | TOPIC: technical | ACTION: Reproduce the blank dashboard and check logs
TKT-103: URGENCY: low | TOPIC: account | ACTION: Confirm identity, then update the email
TKT-104: URGENCY: high | TOPIC: technical | ACTION: Page the on-call engineer for checkout&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The thing that went wrong was a small slip in how the chain was called. The prompt template has one placeholder, {message}, so the input has to be a dict with the key "message". I called the chain with {"ticket": "I was charged twice."} instead, and the run stopped with this error: KeyError: "Input to ChatPromptTemplate is missing variables {'message'}. Expected: ['message'] Received: ['ticket']". The fix is to match the key to the placeholder, chain.invoke({"message": ticket["message"]}), and the error names the missing variable. One surprise: passing a plain string instead of a dict did not fail, because a one-variable template accepts the string as its value.&lt;/p&gt;

&lt;h2&gt;Common mistakes and how to fix them&lt;/h2&gt;

&lt;p&gt;First, passing the wrong key in the input dict. The symptom is KeyError: "Input to ChatPromptTemplate is missing variables {'message'}. Expected: ['message'] Received: ['ticket']", and the chain never reaches the model. The fix is to read the placeholder names in your template and use exactly those keys in the dict you pass to invoke.&lt;/p&gt;

&lt;p&gt;Second, copying LLMChain from an old tutorial. The symptom is a deprecation warning, and in LangChain 1.x the old chain classes are gone from the recommended path. The fix is to rewrite the call as LCEL, prompt | model | parser, as this post does.&lt;/p&gt;

&lt;p&gt;Third, trusting FakeListLLM past its list. The symptom is subtle: the stand-in cycles through its canned replies and starts again from the top, so a fifth ticket silently gets the first ticket's answer. I verified this by calling a one-reply stand-in twice and getting the same reply both times. The fix is to give the stand-in as many replies as calls you plan to make, and to treat it as a wiring test only.&lt;/p&gt;

&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need an API key to try LangChain?&lt;/strong&gt; No. The example here runs fully offline with FakeListLLM, a stand-in returning canned replies, so no account or bill is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is LCEL, and why does every current tutorial use it?&lt;/strong&gt; LCEL is the LangChain Expression Language, the pipe syntax that composes runnables, prompt | model | parser. It replaced LLMChain and SequentialChain, giving every chain streaming, batching, and async support through one interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is LangChain the model itself?&lt;/strong&gt; No. LangChain is the wiring around a model: prompts, chains, parsers, retrievers, and memory. The model still comes from a provider or a local model, and that choice decides cost, speed, and privacy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I validate the chain output as structured data instead of a string?&lt;/strong&gt; Yes, and for a real service you should. LangChain pairs with Pydantic models through output parsers, so the reply comes back as a validated object. The &lt;a href="https://practicaldevblog.blogspot.com/2026/11/pydantic-catch-bad-data-before-it.html" rel="noopener noreferrer"&gt;Pydantic post on this blog&lt;/a&gt; explains that validation pattern.&lt;/p&gt;

&lt;h2&gt;What to remember&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LangChain is wiring around a model, and LCEL (prompt | model | parser) is the current 1.x standard.&lt;/li&gt;
&lt;li&gt;A chain's input is a dict whose keys must match the template placeholders, or you get a KeyError.&lt;/li&gt;
&lt;li&gt;FakeListLLM tests chains offline with no API key, but its replies cycle, so it proves the wiring, not the judgement.&lt;/li&gt;
&lt;li&gt;Old tutorials teaching LLMChain are out of date; rewrite them as LCEL.&lt;/li&gt;
&lt;li&gt;A triage verdict becomes a feature only when it lands in a queue or a database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Where this leaves you&lt;/h2&gt;

&lt;p&gt;You now have a chain that runs and a clear picture of what LangChain adds. The triage example is small on purpose: four tickets, three pieces, one pipe operator, and the whole thing runs offline. From here, swap FakeListLLM for a real chat model on one ticket type, watch what it gets wrong, and tighten the prompt before you add more pieces. If you serve the result over HTTP later, the &lt;a href="https://practicaldevblog.blogspot.com/2026/10/express-explained-nodes-minimal-web.html" rel="noopener noreferrer"&gt;Express post on this blog&lt;/a&gt; shows a small API service. Start with one pipeline and let the chain earn the next piece.&lt;/p&gt;

&lt;h2&gt;Further reading&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;LangChain overview, official docs, &lt;a href="https://python.langchain.com/docs/introduction/" rel="noopener noreferrer"&gt;https://python.langchain.com/docs/introduction/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;LangChain Expression Language, official announcement, &lt;a href="https://www.langchain.com/blog/langchain-expression-language" rel="noopener noreferrer"&gt;https://www.langchain.com/blog/langchain-expression-language&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/11/pydantic-catch-bad-data-before-it.html" rel="noopener noreferrer"&gt;Pydantic: Catch Bad Data Before It Reaches Your Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/10/flask-explained-small-framework-that.html" rel="noopener noreferrer"&gt;Flask Explained: The Small Framework That Does Just Enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://practicaldevblog.blogspot.com/2026/10/express-explained-nodes-minimal-web.html" rel="noopener noreferrer"&gt;Express Explained: Node's Minimal Web Framework&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Waqas Shahid is a software engineer from Karachi with more than 12 years of experience, including 6+ years in engineering leadership. He writes The Practical Developer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/langchain-tutorial-build-llm-app.html" rel="noopener noreferrer"&gt;The Practical Developer&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>python</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>pnpm: One Copy of Every Package, Shared Across Projects</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Fri, 13 Nov 2026 04:51:02 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/pnpm-one-copy-of-every-package-shared-across-projects-5ho</link>
      <guid>https://dev.to/practicaldevblog/pnpm-one-copy-of-every-package-shared-across-projects-5ho</guid>
      <description>&lt;p&gt;Picture a laptop that has hosted a dozen JavaScript projects. The same version of Express sits in a dozen node_modules folders. It works, and on a fast connection nobody notices, until the disk fills up or a CI runner installs everything from scratch on every push. The waste is not dramatic, it is steady: a few minutes here, a few gigabytes there, and a node_modules folder that becomes the biggest thing on the machine. pnpm goes after that waste. It keeps one copy of each package version in a shared store and links it into every project that asks for it. Is that saving worth changing how installs work?&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is how many projects share packages on the same machine. One project with a handful of dependencies gains almost nothing from it. The saving shows up with several projects, a monorepo, or a CI runner that installs fresh each build. If that is your setup, the store earns its place; if not, stay with npm.&lt;/p&gt;

&lt;p&gt;My second check is whether the code uses packages it never declared. npm lays dependencies out flat, so a project can require a package that arrived only as a side effect of something else, and it works by accident. pnpm keeps node_modules strict: only packages listed in package.json are visible at the top level. That strictness catches hidden assumptions before they break a deploy, but the first switch can surface missing declarations to fix, so allow time for cleanup.&lt;/p&gt;

&lt;p&gt;My third check is peer dependencies. pnpm is stricter about them too, and warns or fails where npm silently picked a version. That forces version conflicts into the open, but a tangled peer tree will need them resolved instead of ignored.&lt;/p&gt;

&lt;p&gt;My fourth check is the team and its tooling. pnpm reads the same registry and package.json, and produces a lockfile, pnpm-lock.yaml, that plays the same role as package-lock.json. Most tools work fine with it, but anything assuming a flat node_modules layout, or a script that runs npm install directly, needs updating. Check the CI config first.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use pnpm when several projects share dependencies, or when you want dependency lists you can trust. Skip it for a single small project where npm works. The recommendation flips if the team leans on undeclared packages with no time to clean them up, or if a pipeline tool cannot handle pnpm's layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two projects, one shared copy of Express
&lt;/h2&gt;

&lt;p&gt;The setup is two small projects on the same machine, site-a and site-b, each a plain Express app with exactly one dependency in package.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"site-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"4.21.2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both were installed with pnpm 10.14.0 on Node v24.22.1. The first install in site-a downloaded Express and its 68 transitive dependencies, 69 packages in total, and finished in under four seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Packages: +69
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

dependencies:
+ express 4.21.2 (5.3.0 is available)

Done in 3.8s using pnpm v10.14.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second install, in site-b with the identical dependency, also finished in 3.8 seconds, and it did not need a second download, because the packages were already in the store. The store lives at a single path on this machine, /home/hatch/.local/share/pnpm/store/v10, and it held one 4.0M copy of everything both projects needed. Inside each project, node_modules/express is not a copy at all, it is a symlink:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/express -&amp;gt; .pnpm/express@4.21.2/node_modules/express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain du comparison looks confusing at first: du reported 4.5M for each pnpm node_modules folder and 3.9M for an npm install of the same Express version, with an apparent size of 2.2M in all three cases. The numbers are noisy because pnpm links files from the store with hard links, and du counts a hard-linked file once per folder it appears in. The honest measure is the store itself: one 4.0M copy served both projects, while npm copied the files into each project separately. The saving grows with every extra project that shares the same versions.&lt;/p&gt;

&lt;p&gt;To prove the install actually worked, site-a runs a tiny server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'express'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Hello from site-a'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'express 4.21.2 via pnpm'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Listening on port 3456'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A curl request to the running server returned the JSON exactly as written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hello from site-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"express 4.21.2 via pnpm"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing that went wrong came from that strict node_modules layout. Express itself depends on a package called debug, so debug is present on disk inside node_modules/.pnpm, but it is not listed in site-a's package.json. Requiring it directly failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module 'debug'
Require stack:
- /home/hatch/workspace/.toolchains/verify/pnpm-demo/site-a/[eval]
    at Module._resolveFilename (node:internal/modules/cjs/loader:1564:15)
    at wrapResolveFilename (node:internal/modules/cjs/loader:1118:27)
    at defaultResolveImplForCJSLoading (node:internal/modules/cjs/loader:1142:10)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1169:12)
    at Module._load (node:internal/modules/cjs/loader:1341:5)
    at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
    at Module.require (node:internal/modules/cjs/loader:1674:12)
    at [eval]:1:1
    at runScriptInThisContext (node:internal/vm:219:10) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/home/hatch/workspace/.toolchains/verify/pnpm-demo/site-a/[eval]'
  ]
}

Node.js v24.22.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same require in the npm comparison project loaded debug without complaint, version 2.6.9, because npm's flat layout leaves every transitive package within reach. That is the accident pnpm refuses to repeat: code that depends on debug should say so. The fix is to declare it, then install again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"4.21.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"4.3.7"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After pnpm install reported "Packages: +1", the same require worked and reported version 4.3.7, the version actually declared, rather than the older 2.6.9 that Express happens to use internally. The error felt annoying in the moment, and it was doing exactly what the tool promises: the dependency list now says what the code really needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, the phantom dependency above. The symptom is Error: Cannot find module 'debug' with code MODULE_NOT_FOUND, for a package sitting under node_modules. The fix is to declare the package in package.json and run pnpm install, never reach into .pnpm by hand.&lt;/p&gt;

&lt;p&gt;Second, a lockfile mismatch in CI. The symptom is a failed install saying "specifiers in the lockfile don't match specifiers in package.json" and a line like "express (lockfile: 4.21.2, manifest: 4.21.1)". The fix is to run pnpm install locally so the lockfile updates, then commit it; never hand-edit a version without reinstalling.&lt;/p&gt;

&lt;p&gt;Third, mixing npm and pnpm in one project. The symptom is two lockfiles, package-lock.json beside pnpm-lock.yaml, and installs that behave differently on different machines. The fix is to delete package-lock.json and node_modules, run pnpm install, and keep only one lockfile in the repo.&lt;/p&gt;

&lt;p&gt;Fourth, judging the store by du on node_modules. The symptom is a node_modules folder that still reports several megabytes, as if the saving never happened. The fix is to measure the store path with du instead, since hard links make per-project folders look bigger than the disk they use.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on pnpm
&lt;/h2&gt;

&lt;p&gt;pnpm earns its place on a machine with several JavaScript projects, or a team that wants dependency lists it can trust. The shared store is a real saving, and the strict layout turns silent accidents into loud, fixable errors at install time instead of on a deploy. For one small project with npm already working, switching buys little and adds a new tool to learn. If your setup looks like the two projects above, try it on one of them first: install, run the server, and see whether anything in the code was quietly depending on a package it never declared. If the list comes back clean, keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why pnpm, &lt;a href="https://pnpm.io/motivation" rel="noopener noreferrer"&gt;https://pnpm.io/motivation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Installing pnpm, &lt;a href="https://pnpm.io/installation" rel="noopener noreferrer"&gt;https://pnpm.io/installation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pnpm add, &lt;a href="https://pnpm.io/cli/add" rel="noopener noreferrer"&gt;https://pnpm.io/cli/add&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The npm package manager, Node.js docs, &lt;a href="https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager" rel="noopener noreferrer"&gt;https://nodejs.org/en/learn/getting-started/an-introduction-to-the-npm-package-manager&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/pnpm-one-copy-of-every-package-shared.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/pnpm-one-copy-of-every-package-shared.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pnpm</category>
      <category>tools</category>
    </item>
    <item>
      <title>DuckDB: Analytics Queries on a File, Without a Server</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Fri, 13 Nov 2026 04:49:38 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/duckdb-analytics-queries-on-a-file-without-a-server-2k8k</link>
      <guid>https://dev.to/practicaldevblog/duckdb-analytics-queries-on-a-file-without-a-server-2k8k</guid>
      <description>&lt;p&gt;A small shop owner exports twelve orders to a CSV file and wants one answer: which city brought in the most money this week? The file sits on a laptop, the data fits in a spreadsheet, and yet getting the answer takes longer than it should. A spreadsheet means wrestling with filters and formulas, and the totals shift every time a row is added. A database server for a file this small is worse: installing, configuring, creating users, all for twelve rows. DuckDB sits in the middle. It is a small analytical database that runs inside your own program, reads the CSV directly, and answers with plain SQL. No server, no setup, no separate process to keep alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is the shape of the question. DuckDB earns its place when the question is analytical: totals by group, rankings, running sums, joins across files. If the job is storing one record at a time and reading it back by key, SQLite or a JSON file is simpler, because that is not what an analytical engine is built for.&lt;/p&gt;

&lt;p&gt;DuckDB shines on files from thousands of rows up to hundreds of millions, far beyond what a spreadsheet handles comfortably. For twelve rows it is still pleasant, because setup costs close to zero. For a dataset that no longer fits on one machine, or many writers updating the same tables at once, it is the wrong tool, and a server database or a distributed engine fits better.&lt;/p&gt;

&lt;p&gt;DuckDB speaks SQL, so anyone comfortable with SELECT, GROUP BY, and window functions can use it on day one. A team that only knows pandas can get the same answers with a DataFrame, but SQL keeps the logic in one place when the analysis grows from one file to several.&lt;/p&gt;

&lt;p&gt;DuckDB reads CSV, Parquet, and JSON files directly, and it can query a file without importing it first. That matters when the source file is produced by another system every day and copying it into a database would add a step that can break.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use DuckDB when you have files to analyse with SQL and no appetite for running a server. Skip it when you need many concurrent writers, or when the data has outgrown a single machine. The recommendation flips the moment the file becomes a shared, constantly updated system of record instead of a snapshot to analyse.&lt;/p&gt;

&lt;h2&gt;
  
  
  One small shop, run end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small online shop selling computer accessories. Its orders live in a CSV file with twelve rows: an order id, a city, an item, a quantity, and a price. The cities are Karachi, Lahore, and Islamabad, and the prices are ordinary shop prices, from a Rs 650 cable to a Rs 7,200 monitor arm. The owner wants revenue per city, ranked, and a grand total. The whole script is short, and it reads the CSV straight into DuckDB without loading it into Python first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;

&lt;span class="n"&gt;csv_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"""order_id,city,item,quantity,price
1,Karachi,Wireless mouse,2,1450
2,Lahore,USB-C cable,5,650
3,Karachi,Mechanical keyboard,1,"1,250"
4,Islamabad,Laptop stand,3,2800
5,Lahore,Wireless mouse,1,1450
6,Karachi,Webcam,2,3900
7,Islamabad,USB-C cable,4,650
8,Karachi,Monitor arm,1,7200
9,Lahore,Laptop stand,2,2800
10,Islamabad,Mechanical keyboard,1,"1,250"
11,Karachi,USB-C cable,3,
12,Lahore,Webcam,1,3900
"""&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"orders.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CREATE TABLE orders AS SELECT * FROM read_csv('orders.csv')"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rows in that file are deliberately messy, the way real export files are. Order 3 and order 10 carry the price as "1,250", with a thousands comma, and order 11 has no price at all, because the shop had not entered it yet. DuckDB reads the file and guesses the column types from the values it sees, and the comma in "1,250" means the price column comes in as text rather than a number. The describe output from the run shows this plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Columns and types DuckDB inferred:
- order_id: BIGINT
- city: VARCHAR
- item: VARCHAR
- quantity: BIGINT
- price: VARCHAR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing that went wrong came next. The obvious query, summing the price after casting it to a number, failed on the very first comma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="nc"&gt;Trying&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;obvious&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="kt"&gt;DOUBLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nc"&gt;Conversion&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Conversion&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;convert&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="s1"&gt;'1,250'&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="kt"&gt;DOUBLE&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;casting&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;

&lt;span class="no"&gt;LINE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="kt"&gt;DOUBLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
                   &lt;span class="o"&gt;^^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to clean the value before casting it. REPLACE strips the comma, CAST turns the result into a number, and COALESCE turns the empty price into zero so the row still counts instead of vanishing from the total. The fixed query also ranks the cities with a window function, which is one query doing the grouping and the ranking together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"""
    SELECT
        city,
        COUNT(*) AS orders,
        SUM(quantity) AS items_sold,
        SUM(COALESCE(CAST(REPLACE(price, ',', '') AS DOUBLE), 0) * quantity) AS revenue,
        RANK() OVER (ORDER BY SUM(COALESCE(CAST(REPLACE(price, ',', '') AS DOUBLE), 0) * quantity) DESC) AS revenue_rank
    FROM orders
    GROUP BY city
    ORDER BY revenue DESC
"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"- {city}: {orders} orders, {items} items, Rs {revenue:,.0f}, rank {rank}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the real output from the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total revenue per city (comma stripped, empty price treated as 0):
- Karachi: 5 orders, 9 items, Rs 19,150, rank 1
- Lahore: 4 orders, 9 items, Rs 14,200, rank 2
- Islamabad: 3 orders, 8 items, Rs 12,250, rank 3

Grand total revenue:
Rs 45,600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers check out by hand. Karachi leads because of the Rs 7,200 monitor arm and two webcams, while Islamabad trails even though it sold eight items, because four of them were low-priced cables. Order 11, the one with the missing price, contributed three cables at Rs 0, which is honest about what the data actually says, but it is also a flag: a zero in the total is a reminder to go back and fill in the real price. A cleaner long-term fix is to pass thousands = ',' to read_csv so DuckDB parses "1,250" as a number on the way in, and then the REPLACE is not needed at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, trusting the types DuckDB guesses. The symptom is a Conversion Error on a cast, or a SUM failing because the column arrived as VARCHAR. The fix is DESCRIBE after loading, and declaring the types with the columns option in read_csv when the guess is wrong.&lt;/p&gt;

&lt;p&gt;Second, letting NULLs eat your totals. The symptom is revenue lower than expected, with no error, because a missing price makes the product NULL, and SUM skips NULLs silently. The fix is COALESCE around the value, as in the example, and count NULL prices so you know how much is guesswork.&lt;/p&gt;

&lt;p&gt;Third, forgetting that an in-memory database disappears. The symptom is a table that exists during the run and vanishes next time, because duckdb.connect() with no file name keeps everything in memory. The fix is to pass a file name, such as duckdb.connect("shop.duckdb"), when the data should survive.&lt;/p&gt;

&lt;p&gt;Fourth, querying the file instead of the table. The symptom is a query that re-reads the CSV on every run, so a changed file rewrites old answers. The fix is loading the file once with CREATE TABLE AS, then querying the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on DuckDB
&lt;/h2&gt;

&lt;p&gt;DuckDB is at its best when the data already exists as files and the question is analytical. Twelve messy orders became a ranked revenue table with two queries and no server, and the one real problem, a comma in a price, surfaced as a clear error with a clear fix. For a shared system that many people update at once, or data that no longer fits on one machine, reach for a server database instead. If your work looks like this shop, a pile of exports and a question about totals, try it on one real file and see whether the SQL reads more clearly than the spreadsheet it replaces. If it does, keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;DuckDB Python API, &lt;a href="https://duckdb.org/docs/2.0/clients/python/overview.html" rel="noopener noreferrer"&gt;https://duckdb.org/docs/2.0/clients/python/overview.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;DuckDB CSV Import, &lt;a href="https://duckdb.org/docs/2.0/data/csv/overview.html" rel="noopener noreferrer"&gt;https://duckdb.org/docs/2.0/data/csv/overview.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/duckdb-analytics-queries-on-file.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/duckdb-analytics-queries-on-file.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>duckdb</category>
      <category>database</category>
    </item>
    <item>
      <title>tRPC: Your API and Your Client, Sharing One Type</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Fri, 13 Nov 2026 04:48:21 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/trpc-your-api-and-your-client-sharing-one-type-ooo</link>
      <guid>https://dev.to/practicaldevblog/trpc-your-api-and-your-client-sharing-one-type-ooo</guid>
      <description>&lt;p&gt;Every team that builds a TypeScript API hits the same frustration. The server changes a field name, or a procedure starts expecting a different argument, and the client keeps calling the old shape. Nothing complains until a user hits the broken page in production. The usual fix is a shared schema file, a code generator, or an OpenAPI document that someone has to remember to regenerate. tRPC takes a different route. It skips the schema file entirely and lets the client import the server router type, so the editor knows exactly which procedures exist and what each one accepts. The question is what it costs in practice, and whether the trade is worth it for a small team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is whether the whole stack is TypeScript. tRPC only shares types between a TypeScript server and a TypeScript client, so a mobile app in another language gets nothing from it. If not, you still need a documented HTTP contract, and tRPC adds a second one.&lt;/p&gt;

&lt;p&gt;My second check is who the API is for. An internal API that only your own frontend calls is the sweet spot: you control both ends, and a server rename shows up as a red squiggle on the client the same afternoon. A public API is the opposite. Strangers need stable docs and versioning they can read without your source code, and tRPC gives them none of that.&lt;/p&gt;

&lt;p&gt;My third check is how the team feels about framework coupling. tRPC has a React integration through TanStack Query, and it works with plain Express too. But adopting it ties the shape of your API to one library's idea of routers and procedures, so moving away later means rewriting every call site, not just swapping a fetch wrapper.&lt;/p&gt;

&lt;p&gt;My fourth check is validation. Types disappear at runtime, so a type alone cannot stop a bad request from curl or a hand-written fetch. tRPC pairs with Zod for that, and the example shows the two layers working together: TypeScript catches the mistake in the editor, Zod catches it on the wire.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use tRPC when the client and server are both TypeScript, the API is internal, and the team is tired of syncing a schema file by hand. Skip it for a public API or a mixed-language system. The recommendation flips the moment a non-TypeScript consumer appears, or the API needs a written contract that outlives your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  One order lookup, run end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small order lookup service for an online shop. Three orders live in memory, each with an order id, a customer name, an item, a total in rupees, and a status. The router exposes two procedures: getOrder, which takes an order id and returns one order, and listOrders, which returns all three. The input is validated with Zod, so an order id must be a non-empty string. The server is plain Express with tRPC's Express adapter, and the client imports only the router type, never the server code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;initTRPC&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"@trpc/server"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createExpressMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"@trpc/server/adapters/express"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"zod"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;initTRPC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-1041"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mehwish Raza"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mechanical keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"shipped"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Danish Ali"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"USB-C hub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"processing"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-1043"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hira Naveed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Laptop stand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"delivered"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;procedure&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`No order found with id ${input.orderId}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;listOrders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;procedure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/trpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;createExpressMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tRPC server listening on http://localhost:4000/trpc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client is short, because the type does most of the work. Importing AppRouter as a type gives the client full autocomplete for getOrder and listOrders, and the editor knows the shape of the returned order without any generated code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createTRPCProxyClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpBatchLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"@trpc/client"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"./server"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createTRPCProxyClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppRouter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;httpBatchLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:4000/trpc"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-1042"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Order ORD-1042:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Total orders:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the real output from the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;ORD&lt;/span&gt;&lt;span class="mi"&gt;-1042&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;ORD&lt;/span&gt;&lt;span class="mi"&gt;-1042&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;Danish&lt;/span&gt; &lt;span class="n"&gt;Ali&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;USB&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt; &lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;processing&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing that went wrong came from skipping the typed client. A raw HTTP call passed the order id as a number, 42, instead of a string. The server never ran the procedure. Zod rejected the input first, and the response came back with code BAD_REQUEST and HTTP status 400, carrying this message: "Invalid input: expected string, received number". The fix is simple, pass a string, but the more useful point is that the typed client would never have let that call be written. A second client file with the same mistake, orderId: 42, failed type-checking before it ran at all, with this error: client-typo.ts(10,47): error TS2322: Type 'number' is not assignable to type 'string'. That is the two-layer safety net in action. TypeScript guards the code you write, Zod guards the requests that arrive anyway. One more detail from the run is worth knowing: asking for an unknown id, ORD-9999, returned the plain message "No order found with id ORD-9999", but with code INTERNAL_SERVER_ERROR and HTTP 500, because the procedure throws a plain Error. A missing order is not a server crash, and the fix is to throw a TRPCError with code NOT_FOUND instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, passing the wrong argument type from the client. The symptom is error TS2322, "Type 'number' is not assignable to type 'string'", when you run tsc, and the code never compiles. The fix is to match the Zod input schema, which is the single source of truth for what the procedure accepts.&lt;/p&gt;

&lt;p&gt;Second, calling the API with a hand-written fetch or curl and skipping validation. The symptom is a BAD_REQUEST response with a Zod message like "Invalid input: expected string, received number". The fix is to use the typed client where possible, and to treat the Zod schema as the contract, not as decoration.&lt;/p&gt;

&lt;p&gt;Third, throwing a plain Error for a missing record. The symptom is a 500 INTERNAL_SERVER_ERROR for something that is really a 404, which confuses both monitoring and the frontend. The fix is to import TRPCError from @trpc/server and throw new TRPCError({ code: "NOT_FOUND", message: ... }) so the client gets the right status.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on tRPC
&lt;/h2&gt;

&lt;p&gt;tRPC earns its place when both ends speak TypeScript and the API belongs to your team. The order lookup needed one router, one Zod schema, and a typed client, and a wrong argument type was caught twice, once by the compiler and once by the validator. That is a genuine saving over a hand-kept schema file. It stops being the right tool when the API has to serve strangers or other languages, because the shared type is invisible to the people who need the contract most. If your setup looks like this shop, try it on one router and see whether the editor feedback feels like help or noise. If it feels like help, keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;tRPC Quickstart, &lt;a href="https://trpc.io/docs/quickstart" rel="noopener noreferrer"&gt;https://trpc.io/docs/quickstart&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;tRPC Input and Output Validators, &lt;a href="https://trpc.io/docs/server/validators" rel="noopener noreferrer"&gt;https://trpc.io/docs/server/validators&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript Handbook, The Basics, &lt;a href="https://www.typescriptlang.org/docs/handbook/2/basic-types.html" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/docs/handbook/2/basic-types.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/trpc-your-api-and-your-client-sharing.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/trpc-your-api-and-your-client-sharing.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>trpc</category>
      <category>api</category>
    </item>
    <item>
      <title>esbuild: Bundle JavaScript in Milliseconds, Not Seconds</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Thu, 12 Nov 2026 04:57:32 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/esbuild-bundle-javascript-in-milliseconds-not-seconds-48k9</link>
      <guid>https://dev.to/practicaldevblog/esbuild-bundle-javascript-in-milliseconds-not-seconds-48k9</guid>
      <description>&lt;p&gt;You change one line in a small project, save, and wait. The bundler thinks, the terminal scrolls, and a few seconds pass before the page refreshes. Do that forty times in an afternoon and you have lost several minutes to staring at a spinner, plus the focus you lose every time the flow breaks. Most bundlers are slow because they are written in JavaScript and parse everything with a JavaScript parser. esbuild is written in Go, parses in parallel, and treats bundling as a compile step rather than a scripting task. The question is whether that speed is worth changing tools for, and what you give up in return.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is how much the build actually hurts. Two files bundle in milliseconds under any tool. The pain starts with hundreds of modules and a rebuild on every save. If a rebuild already takes seconds, esbuild's speed is the whole point. If builds are already under a second, keep what you have.&lt;/p&gt;

&lt;p&gt;My second check is what the project needs besides bundling. esbuild handles JavaScript, TypeScript, JSX, CSS, and minification, which covers most apps. It does not type check TypeScript, so a type error sails through the build, and you need a separate tsc run. It has no dev server with hot module replacement, only watch mode and a small serve mode. If the project leans on a framework's dev server, a bigger tool still fits better.&lt;/p&gt;

&lt;p&gt;My third check is how much configuration the team wants. esbuild is a command and a handful of flags, a relief after a long config file. The trade is that anything unusual needs a plugin, and the plugin ecosystem is smaller than the one around webpack or Vite. Count the custom steps in the current build first. If the list is short, esbuild covers it.&lt;/p&gt;

&lt;p&gt;My fourth check is the output target. esbuild bundles for the browser and for Node, and this post uses the Node target, which suits command line tools and small servers.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use esbuild when the build is slow enough to notice and the project needs bundling, minifying, and TypeScript stripping rather than a framework's dev server. Skip it when you depend on hot module replacement or heavy plugins. The recommendation flips when the plugin list outgrows the source list, or when the team needs type checking inside the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shop price calculator, bundled end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small shop price calculator in two files. The entry file, index.js, holds the order and prints the receipt. A helper module, pricing.js, computes line totals, a 10 percent discount on orders over $100, and 8 percent tax. Three tote bags at $18, three mugs at $14.50, and two notebook sets at $9.25 come to a subtotal of $116, so the discount kicks in, which makes the numbers worth checking by hand.&lt;/p&gt;

&lt;p&gt;Here is pricing.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lineTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addTax&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is index.js, which imports those three functions and runs the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;lineTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addTax&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./pricing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;18.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;9.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lineTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${item.qty} x ${item.name} = $${total.toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;afterDiscount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addTax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;afterDiscount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;grandTotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;afterDiscount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Subtotal: $${subtotal.toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Discount (10% over $100): -$${discount.toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Tax (8%): $${tax.toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Grand total: $${grandTotal.toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build command is short: bundle the entry file, target Node, and write one output file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./node_modules/.bin/esbuild index.js --bundle --platform=node --outfile=bundle.cjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the real build output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  bundle.cjs  1.5kb

Done in 4ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four milliseconds, and a single 1,496 byte file. Opening bundle.cjs shows both files inlined into one: the pricing helpers first, then the entry code, with a small helper at the top that wires the CommonJS modules together. Nothing else is in there, because the only dependency is the project's own helper file. Running the bundle with Node gives the real receipt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 x Canvas tote bag = $54.00
3 x Ceramic mug = $43.50
2 x Notebook set = $18.50
Subtotal: $116.00
Discount (10% over $100): -$11.60
Tax (8%): $8.35
Grand total: $112.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers check out by hand: $116 minus the $11.60 discount is $104.40, and 8 percent tax on that is $8.35, giving $112.75. Running the original index.js directly prints exactly the same receipt, which is the test that matters: the bundle behaves like the source.&lt;/p&gt;

&lt;p&gt;The thing that went wrong came before the successful build. My first attempt had a typo in the import, require("./price") instead of require("./pricing"), and the build stopped at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ERROR] Could not resolve "./price"

    index.js:1:53:
      1 | const { lineTotal, applyDiscount, addTax } = require("./price");
        |                                                      ~~~~~~~~~

1 error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build exited with code 1 and wrote no bundle file, which is the correct behaviour: a failed build should not leave a stale bundle behind for you to run by mistake. The fix was the missing "ing" on the file name, and the next build finished in 4ms. The lesson is small but worth keeping: esbuild reports the bad import with the file and line number, so a broken path is a ten second fix rather than a hunt through the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, forgetting the --bundle flag. Without it, esbuild still writes an output file, but the require("./pricing") line stays in place, so the bundle is not self contained. It runs fine while pricing.js sits next to it, then fails the moment you ship the bundle alone, with "Error: Cannot find module './pricing'". The fix is to always pass --bundle when you want one file, and to test the bundle in a folder of its own.&lt;/p&gt;

&lt;p&gt;Second, forgetting --platform=node when bundling for Node. esbuild targets the browser by default, so a file that uses a Node built-in module stops the build with "Could not resolve 'fs'", plus a hint to use --platform=node. The fix is to name the platform in the command, and to keep browser and Node builds as separate commands.&lt;/p&gt;

&lt;p&gt;Third, assuming esbuild type checks TypeScript. It strips types and bundles the code without checking them, so a wrong type in a .ts file builds cleanly and only fails later, in tests or at runtime. The fix is to run tsc --noEmit as a separate step in the same script that runs the build, so type errors still block a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on esbuild
&lt;/h2&gt;

&lt;p&gt;esbuild does one job, bundling, and it does it fast enough that the build step stops being something you wait for. The calculator above went from two files to a single 1.5kb bundle in 4ms, and the bundle printed the same receipt as the source, which is all a bundler has to prove. Its limits are plain: no type checking, no hot module replacement, and a smaller plugin world than the big tools. For a small app, a command line tool, or a library build, that trade is easy to take. Try it on one project and time it against your current tool. If the difference is seconds per rebuild, keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;esbuild documentation, Getting Started, &lt;a href="https://esbuild.github.io/getting-started/" rel="noopener noreferrer"&gt;https://esbuild.github.io/getting-started/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;esbuild documentation, API reference, &lt;a href="https://esbuild.github.io/api/" rel="noopener noreferrer"&gt;https://esbuild.github.io/api/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;esbuild documentation, JavaScript API, &lt;a href="https://esbuild.github.io/api/js/" rel="noopener noreferrer"&gt;https://esbuild.github.io/api/js/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/esbuild-bundle-javascript-in.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/esbuild-bundle-javascript-in.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>esbuild</category>
      <category>bundling</category>
    </item>
    <item>
      <title>React Router: Pages That Change Without Reloading</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Thu, 12 Nov 2026 04:56:05 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/react-router-pages-that-change-without-reloading-2a6o</link>
      <guid>https://dev.to/practicaldevblog/react-router-pages-that-change-without-reloading-2a6o</guid>
      <description>&lt;p&gt;Open a shop site and click a product. The address bar changes, the page changes, and nothing reloads. Visitors expect that smooth switch now, and it is what makes a single-page app feel finished rather than half built. Without a router, the usual workaround is a pile of state: a variable holding the current view, a switch statement rendering one component or another, and links that are really buttons wearing a disguise. It works for two pages, then starts to creak. The address bar never changes, so a visitor cannot bookmark a product, share its link, or use the back button properly. React Router closes exactly that gap, and the question is when it is worth adding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is how many views the app really has. One page with a couple of panels does not need a router, and adding one just adds a dependency to keep updated. Three or more distinct views is where a router starts earning its place. Count the pages before reaching for the library.&lt;/p&gt;

&lt;p&gt;My second check is whether the URLs need to mean anything. A shop lives on shareable links: a customer sends a product page to a friend, a search engine indexes it, a visitor bookmarks it. If the address bar never changes, none of that works, and the app is quietly broken for everyone who treats a URL as an address. If nobody shares or bookmarks a view, a router buys little.&lt;/p&gt;

&lt;p&gt;My third check is the learning curve, and React Router is gentle here. The example below rests on four ideas: Routes and Route to map paths to components, Link to move between them, and useParams to read a value out of the URL. Anyone who knows React can read that code on the first pass. The cost is a dependency and a little setup.&lt;/p&gt;

&lt;p&gt;My fourth check is the layout. Most apps repeat the same header and footer on every page, and React Router lets one layout component wrap the changing content instead of copying it into every view.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use React Router when an app has several views, when those views deserve real URLs, and when a shared layout would cut repetition. Skip it for a single view or a page that nobody links to. The recommendation flips when the page count drops to one, or the URLs stop mattering, because then the router is a dependency with no job to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  One small shop, three routes, run end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small shop with three routes: the home page at /, a product list at /products, and a product detail page at /products/:id, where :id is a URL parameter read with useParams. The products are a canvas tote bag at $18, a ceramic mug at $14.50, and a notebook set at $9.25. Everything runs in plain Node with MemoryRouter and ReactDOMServer renderToString, so no browser and no build step are needed. MemoryRouter stands in for the BrowserRouter a real app would use, and every Link and useParams call sits inside it, because a Link rendered outside a router throws a cryptic error about a null context instead of rendering anything.&lt;/p&gt;

&lt;p&gt;Here is the whole app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;renderToString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"react-dom/server"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;MemoryRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;useParams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"react-router"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;18.0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;9.25&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"div"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"The Corner Shop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Three useful things, fairly priced."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Browse products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;ProductList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"div"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"li"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" - $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;ProductDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"div"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Product not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"No product has id "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Back to products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"div"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Price: $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Back to products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"div"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Page not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Back home"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/products/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ProductDetail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"=== "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" ==="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MemoryRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;initialEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the real output from running it with Node, for the home page and the ceramic mug detail page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;=== / ===
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;The Corner Shop&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Three useful things, fairly priced.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt; &lt;span class="na"&gt;data-discover&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Browse products&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
=== /products/2 ===
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Ceramic mug&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;&lt;/span&gt;Price: $14.50&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt; &lt;span class="na"&gt;data-discover&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Back to products&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth noticing. First, the URL parameter did real work: visiting /products/2 rendered the ceramic mug and its $14.50 price, pulled from the same products array as the list, so the list and the detail page can never disagree about a price. Second, the Link components rendered as ordinary anchor tags with the right href, which is why the back button and bookmarks keep working: the router is built on plain links, not on a trick.&lt;/p&gt;

&lt;p&gt;The thing that went wrong came from the first version of this app, which had no catch-all route and no guard for a missing product. Visiting an unknown address rendered a blank page, with no error and no message, and visiting /products/999 crashed outright. Here is the real output from that broken run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== /nowhere ===

=== /products/999 ===
ERROR: Cannot read properties of undefined (reading 'name')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blank page is the nastier of the two. The crash at least tells you something failed, but a blank page looks like the app loaded and chose to show nothing, so a visitor just leaves. The fix is two small additions: a route with path "*" that renders a "Page not found" message, and a guard in ProductDetail that checks whether the product exists before reading its name. After adding both, the same paths render properly, as the full run showed: /products/999 gives "Product not found" with a link back to the list, and /nowhere gives "Page not found" with a link home. Unknown URLs and unknown ids are normal traffic, not edge cases, so both guards belong in the app from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, leaving out the catch-all route, as in the broken run above. The symptom is a blank page on any mistyped or outdated URL, with no error anywhere to point at. The fix is a Route with path "*" at the end of the Routes list, rendering a helpful not-found page with a link home.&lt;/p&gt;

&lt;p&gt;Second, trusting the URL parameter without checking it. The symptom is a crash, "Cannot read properties of undefined (reading 'name')", when a visitor opens a product id that does not exist, because find returns undefined and the code reads a property off it. The fix is the guard shown above: look the product up, and if it is missing, render a not-found message instead of the detail view.&lt;/p&gt;

&lt;p&gt;Third, using a plain anchor tag instead of Link for navigation inside the app. The symptom is a full page reload on every click, which throws away the app state and defeats the whole point of a router. The fix is to use Link for every in-app move, and save plain anchor tags for links that leave the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on React Router
&lt;/h2&gt;

&lt;p&gt;React Router turns the address bar into a real part of the app instead of a decoration. The shop above needed four ideas, three routes, and two small guards, and every URL now leads somewhere sensible, including the wrong ones. That is the whole job, and the library does it without asking the app to change shape. Try it on a project with three or more views that people share links to, and watch the back button start behaving. If the app has one view and nobody links to it, skip the router and keep the code plain. The router is worth it when the URL matters, and not a moment sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React Router documentation, &lt;a href="https://reactrouter.com/" rel="noopener noreferrer"&gt;https://reactrouter.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React docs, Learn React, &lt;a href="https://react.dev/learn" rel="noopener noreferrer"&gt;https://react.dev/learn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React docs, Adding Interactivity, &lt;a href="https://react.dev/learn/adding-interactivity" rel="noopener noreferrer"&gt;https://react.dev/learn/adding-interactivity&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/react-router-pages-that-change-without.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/react-router-pages-that-change-without.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>reactrouter</category>
    </item>
    <item>
      <title>SQLAlchemy: Turn Your Python Classes Into Database Tables</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Thu, 12 Nov 2026 04:54:51 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/sqlalchemy-turn-your-python-classes-into-database-tables-2lk7</link>
      <guid>https://dev.to/practicaldevblog/sqlalchemy-turn-your-python-classes-into-database-tables-2lk7</guid>
      <description>&lt;p&gt;Picture a bike repair shop. The owner keeps one notebook for customers, another for bikes, and a third for repair jobs. Each week she works out who owes what, matching names across three notebooks. A spreadsheet helps at first, until the same customer appears under two spellings and a paid job gets counted twice. It is all there, but the links between the pieces live only in her head. That is the point where a Python developer reaches for SQLAlchemy. It turns customers, bikes, and jobs into Python classes backed by tables, with the relationships written down in code instead of remembered. The question is whether it is the right weight for the job in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is how connected the data is. One table with no relationships does not need an ORM, and a plain sqlite3 script would be simpler. Once you have three tables that point at each other, customers owning bikes and bikes having jobs, and questions that join them, the mapping earns its keep.&lt;/p&gt;

&lt;p&gt;My second check is who will read the code. SQLAlchemy lets the team work in Python and treat rows as objects, which suits a team that thinks in classes. But it adds its own ideas, sessions and lazy loading, and a developer who only knows SQL has to learn those before they can debug anything.&lt;/p&gt;

&lt;p&gt;My third check is how the database will change over time. Adding a column is easy on day one and painful on day one hundred, when real customer data is already in the tables. SQLAlchemy pairs with Alembic, its migration tool, so schema changes become versioned scripts instead of hand-edited tables.&lt;/p&gt;

&lt;p&gt;My fourth check is performance. The ORM is fast enough for a shop like this one and slow enough to notice at scale. The classic trap is the N+1 query: loading fifty bikes and then touching each bike's jobs fires fifty extra queries. A reporting dashboard that scans millions of rows belongs in raw SQL.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use SQLAlchemy when a Python app owns several related tables and the code will keep growing with them. Skip it for a one-off script, a single table, or a heavy reporting job. The recommendation flips when the data flattens to one table, or when the team needs to squeeze speed out of bulk queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  One bike shop, run end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small bike repair shop with three tables. A customer has a phone number that must be unique, a bike belongs to one customer, and a repair job belongs to one bike and carries a cost and a status of paid or unpaid. The models below are the whole schema, written as Python classes, and the relationships are what make the join query possible. The script uses a local SQLite file, so it runs anywhere with nothing to install except SQLAlchemy itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;
&lt;span class="nf"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orm&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;declarative_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionmaker&lt;/span&gt;

&lt;span class="nc"&gt;Base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;declarative_base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"customers"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bikes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bike"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Bike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bikes"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;owner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"customers.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bikes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RepairJob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bike"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RepairJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repair_jobs"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bike_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bikes.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "unpaid" or "paid"&lt;/span&gt;
    &lt;span class="n"&gt;bike&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bike"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"jobs"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sqlite:///shop.db"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shop has three customers. Ayesha Khan owns a Trek Marlin 7 and a Giant Escape 3, Bilal Ahmed owns a Hero Ranger 26, and Sara Iqbal owns a Specialized Sirrus X. Six repair jobs are spread across the bikes, from a Rs 900 puncture repair to a Rs 6,500 chain and cassette replacement. Two jobs are already paid, and the rest are outstanding. The question the owner actually asks each week is how much each customer still owes, and that is a join across all three tables: customers to their bikes, bikes to their jobs, filtered to unpaid jobs, grouped by customer.&lt;/p&gt;

&lt;p&gt;The thing that went wrong came before the query. A second record for Ayesha was added with the same phone number, 0301-5550142, which the unique constraint on the phone column forbids. Here is the real output from the run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seeded 3 customers, 4 bikes, 6 repair jobs.
Adding a duplicate customer with Ayesha's phone number...
IntegrityError: UNIQUE constraint failed: customers.phone
Customers after the failed insert: 3
Outstanding repair cost per customer:
- Ayesha Khan: Rs 8,300 outstanding
- Bilal Ahmed: Rs 1,600 outstanding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The duplicate never reached the database, and the customer count stayed at 3, because the code caught the error and called session.rollback(). Without that rollback the session stays broken and the next query fails, so catching the error is only half the fix. The other half is checking for an existing customer by phone before inserting a new one, which is how the shop would stop the same customer being entered twice in real use. After the rollback the join query ran cleanly and gave the answer the owner needed: Ayesha owes Rs 8,300, from an unpaid brake job at Rs 1,800 and an unpaid chain replacement at Rs 6,500, while Bilal owes Rs 1,600. Sara does not appear in the list at all, and that is correct, because her only job, a full service at Rs 3,500, is already paid. The query only joins rows that exist, so a customer with no unpaid jobs simply has no row, and a report that needs to show zero balances would use an outer join instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, forgetting session.commit(). The symptom is data that works fine during the run and vanishes afterwards, because objects added to a session are only staged until they are committed. The fix is to commit after each unit of work and check the database file afterwards.&lt;/p&gt;

&lt;p&gt;Second, using objects after the session closes. The symptom is DetachedInstanceError when you touch a relationship on an object whose session is gone. The fix is to load everything you need, including related rows, before closing the session, or keep the session open until the work is done.&lt;/p&gt;

&lt;p&gt;Third, skipping the rollback after an error. The symptom is PendingRollbackError on the very next query after an IntegrityError, even though the bad row was never saved. The fix is to call session.rollback() in the except block, every time, before doing anything else with that session.&lt;/p&gt;

&lt;p&gt;Fourth, loading related rows one by one. The symptom is a page that slows down as the data grows, because each bike triggers its own query for its jobs. The fix is eager loading with joinedload or selectinload, so the related rows arrive in one or two queries instead of dozens.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on SQLAlchemy
&lt;/h2&gt;

&lt;p&gt;SQLAlchemy is at its best when the data has shape: tables that point at each other, rules like a unique phone number that belong in the database, and questions that join across those tables. The bike shop needed a short set of models, a session, and one query, and the duplicate phone number was caught by the database. For a single flat table or a report that scans millions of rows, plain SQL is shorter and faster. If your data looks like this shop, try it on one small set of tables and see whether the code reads more clearly than the SQL it replaces. If it does, keep going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SQLAlchemy ORM Quick Start, &lt;a href="https://docs.sqlalchemy.org/en/20/orm/quickstart.html" rel="noopener noreferrer"&gt;https://docs.sqlalchemy.org/en/20/orm/quickstart.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SQLAlchemy ORM Mapping, &lt;a href="https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html" rel="noopener noreferrer"&gt;https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SQLAlchemy Session Basics, &lt;a href="https://docs.sqlalchemy.org/en/20/orm/session_basics.html" rel="noopener noreferrer"&gt;https://docs.sqlalchemy.org/en/20/orm/session_basics.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/sqlalchemy-turn-your-python-classes.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/sqlalchemy-turn-your-python-classes.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>sqlalchemy</category>
      <category>database</category>
    </item>
    <item>
      <title>Python asyncio: Running Jobs at the Same Time Without Threads</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Wed, 11 Nov 2026 04:39:10 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/python-asyncio-running-jobs-at-the-same-time-without-threads-4chb</link>
      <guid>https://dev.to/practicaldevblog/python-asyncio-running-jobs-at-the-same-time-without-threads-4chb</guid>
      <description>&lt;p&gt;Picture a small shop dashboard on a Monday morning. Before the first order ships, someone has to check stock with four suppliers, one for tote bags, one for mugs, one for notebooks, one for lamps. Each check takes a couple of seconds while the supplier's system answers. Done one after another, that is eight seconds of staring at a spinner, and the computer spends almost all of it doing nothing, just waiting for a reply. The work is not hard, it is slow, and the slowness comes from waiting, not from thinking. That is the exact gap asyncio is built for: one program that starts several waits at once and picks each one up when its answer arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is what the program is actually waiting on. Asyncio helps when the time goes to waiting: network calls, database queries, slow file reads. It does nothing for work that keeps the processor busy, like crunching numbers or resizing images, because one thread still does that work one piece at a time. If the job is mostly computing, threads or separate processes are the honest answer, not asyncio.&lt;/p&gt;

&lt;p&gt;My second check is whether the libraries I need can play along. Asyncio only works if the waiting code is written for it. A normal requests call blocks the whole loop while it waits, which quietly turns a concurrent program back into a sequential one. So I check for an async client, like aiohttp for HTTP or asyncpg for Postgres, before I commit. If the only library available is synchronous, asyncio buys me nothing.&lt;/p&gt;

&lt;p&gt;My third check is how many things run at once, and who reads the code later. Four supplier checks are easy to reason about. Four hundred are still possible, but then I need to limit how many run together, or the program hammers the supplier's server. Async code is also harder to debug: an error can surface in a different task from the one that caused it. For a script I run once a month, sequential code is easier to trust.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use asyncio when a program makes several independent waits and the libraries support it. That is the shape of most small dashboards, scrapers, and notification senders. The recommendation flips the moment there is only one wait, the work is mostly computing, or the libraries are synchronous. In those cases the plain version is shorter, easier to debug, and just as fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four orders, checked at the same time
&lt;/h2&gt;

&lt;p&gt;The example is a small shop checking stock for four orders before the day starts. Each check waits on a supplier's system, and the waiting time is different for each one: a second and a half for the tote bags, two seconds for the mugs, one second for the notebooks, two and a half for the lamp. In the script, asyncio.sleep stands in for that waiting, since there is no real supplier to call. The real question is only how long four waits take when they run together, and the script prints the answer.&lt;/p&gt;

&lt;p&gt;Here is the whole program. The check_stock coroutine waits, then reports the order as in stock. The main coroutine hands all four checks to asyncio.gather, which starts them together and waits until every one of them has finished. The timer around it measures the whole run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="c1"&gt;# Four shop orders. The sleep stands in for the wait while a&lt;/span&gt;
&lt;span class="c1"&gt;# supplier's stock service answers; each one is slow on its own.&lt;/span&gt;
&lt;span class="no"&gt;ORDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ayesha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-502"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bilal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-503"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Sara"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ORD-504"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Danish"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Desk lamp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"delay"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;'{order["order"]} ({order["customer"]}, {order["item"]}): in stock'&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;check_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;ORDERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Checked {len(ORDERS)} orders concurrently in {elapsed:.2f} seconds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the real output from running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORD-501 (Ayesha, Canvas tote bag): in stock
ORD-502 (Bilal, Ceramic mug): in stock
ORD-503 (Sara, Notebook set): in stock
ORD-504 (Danish, Desk lamp): in stock
Checked 4 orders concurrently in 2.50 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four results came back, and the whole run took 2.50 seconds. That is not the sum of the four waits, it is the longest single wait, the two and a half seconds for the lamp. While the lamp check was waiting, the other three finished and reported in. That is the entire trick: overlapping the waits instead of stacking them.&lt;/p&gt;

&lt;p&gt;The thing that went wrong was my first version of this script. I wrote the natural loop, awaiting each check inside it, and the program checked one order, waited for the answer, then moved on to the next. It worked, and it took the full seven seconds, one wait stacked on the next. Here is that version, with its output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;ORDERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Awaiting inside the loop: each check waits for the last one.&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;check_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Checked {len(ORDERS)} orders one by one in {elapsed:.2f} seconds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORD-501 (Ayesha, Canvas tote bag): in stock
ORD-502 (Bilal, Ceramic mug): in stock
ORD-503 (Sara, Notebook set): in stock
ORD-504 (Danish, Desk lamp): in stock
Checked 4 orders one by one in 7.01 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was one line: gather the four coroutines instead of awaiting them in a loop. Same checks, same waits, same results, but the elapsed time dropped from 7.01 seconds to 2.50. The lesson is that await inside a loop means finish this one, then start the next, while gather means start them all, and tell me when the last one finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, forgetting await. Calling a coroutine without awaiting it does not run it, it hands back a coroutine object and prints a warning at shutdown. This is the real output from a script that made that mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Got: &amp;lt;coroutine object check_stock at 0x70cc65761840&amp;gt;
RuntimeWarning: coroutine 'check_stock' was never awaited
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to await the call, or pass it to asyncio.gather, and to treat that warning as a bug, not noise, because the check never ran at all.&lt;/p&gt;

&lt;p&gt;Second, awaiting inside a loop, as in the example above. The symptom is a program that works but takes the full summed time, 7.01 seconds here instead of 2.50. The fix is asyncio.gather, or asyncio.create_task for checks that should start right away.&lt;/p&gt;

&lt;p&gt;Third, a timeout that cancels the task. Wrapping a slow check in asyncio.wait_for with a short timeout raises TimeoutError and cancels the check, so the order never gets an answer. This is the real output from a check given half a second for a two second wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timed out waiting for ORD-502 after 0.5 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to catch TimeoutError and decide: retry once, mark the order as unchecked, or allow a longer wait, instead of letting the exception end the whole run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict, after timing it
&lt;/h2&gt;

&lt;p&gt;Asyncio is worth learning the moment a program waits on more than one thing. The example above is the whole case in miniature: same four checks, same waits, 7.01 seconds one way and 2.50 the other, with no extra libraries and no threads to manage. The price is a new habit: every wait has to be awaited, and the code has to be written for the loop. If your next script spends its time waiting on a network or a database, try it on that one script first. Run it, time it, and let the elapsed seconds make the decision for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python documentation, asyncio, &lt;a href="https://docs.python.org/3/library/asyncio.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Python documentation, Coroutines and Tasks, &lt;a href="https://docs.python.org/3/library/asyncio-task.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio-task.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/python-asyncio-running-jobs-at-same.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/python-asyncio-running-jobs-at-same.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>programming</category>
    </item>
    <item>
      <title>Fastify: A Fast Node API Without the Ceremony</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Wed, 11 Nov 2026 04:37:41 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/fastify-a-fast-node-api-without-the-ceremony-3ab5</link>
      <guid>https://dev.to/practicaldevblog/fastify-a-fast-node-api-without-the-ceremony-3ab5</guid>
      <description>&lt;p&gt;Picture a small shop that takes orders through a web page. A customer fills in a form, the browser sends the order as JSON, and a Node server has to check the fields, save the order, and answer with the new order id. The work itself is small. The ceremony around it is not: parsing the body, checking that every required field is present, sending back the right status code when something is missing, and keeping the routes readable as they grow. Express leaves most of that to you, which is fine until the third route starts looking like the first two. That gap is where Fastify earns a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is validation. Fastify lets me describe the shape of a request body as a JSON schema, and it rejects bad requests before my handler runs. That saves manual checks in every route, and the client gets a clear 400 instead of a crash later. If the API only takes one or two simple fields, the schema costs more setup than it saves.&lt;/p&gt;

&lt;p&gt;My second check is speed. Fastify is widely quoted as a very fast Node framework. I treat that as a bonus, not a reason to switch, because a small shop API is rarely slow at the framework level; it is slow at the database, or at a call to another service. Pick Fastify for the developer experience; let the speed be a pleasant extra.&lt;/p&gt;

&lt;p&gt;My third check is the plugin system. Fastify routes live in plugins, which keeps a growing API organised: one plugin for orders, one for customers, each with its own prefix. A flat file of routes turns into a mess around the tenth endpoint. The trade-off is learning encapsulation: a plugin's decorations stay hidden from the rest of the app unless you register them the right way.&lt;/p&gt;

&lt;p&gt;My fourth check is the ecosystem. Express has more middleware and more answers already written for it. Fastify covers the common pieces, CORS, static files, and websockets, but a niche Express middleware may have no Fastify twin.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use Fastify for a JSON API with several endpoints when you want validation and structure built in. Skip it for a single tiny endpoint, or when your team knows Express and the deadline is tight. The recommendation flips the moment the API shrinks to one route, or the project needs an Express-only library you cannot replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  One small shop, served end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small shop orders API. It runs on my own machine, with no real shop and no internet service behind it, and it does the three jobs a real orders page needs: list the orders, load one order by id, and create a new order. The new order route carries a JSON schema, so Fastify checks the body before the handler runs. The test uses fastify.inject(), which calls the routes in memory, so no separate client process is needed.&lt;/p&gt;

&lt;p&gt;Here is the whole server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fastify"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ayesha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;36.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"shipped"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bilal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"processing"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Sara"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;27.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"delivered"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/orders/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Order not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;orderSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;orderSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;nextId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nextId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in that code are worth a pause. First, the handler returns the data instead of calling reply.send(), and Fastify serialises the returned value as JSON. That is the normal Fastify style, and it keeps handlers short. Second, the schema sits beside the route, not inside it, so the validation rules are visible at a glance and the handler only deals with a body that already passed.&lt;/p&gt;

&lt;p&gt;Here is the test script, using inject() to call each route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./server"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/orders"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GET /orders status:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Body:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/orders/102"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\nGET /orders/102 status:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Body:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/orders/999"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\nGET /orders/999 status:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Body:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hassan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Leather wallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\nPOST /orders (missing total) status:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Body:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hassan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Leather wallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;42.5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"\nPOST /orders (complete) status:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Body:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the real output from running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ayesha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"shipped"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bilal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;14.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Sara"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;27.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"delivered"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bilal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;14.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Order not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"FST_ERR_VALIDATION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bad Request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"body must have required property 'total'"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
&lt;span class="nc"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;104&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hassan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Leather wallet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;42.5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing that went wrong was the first POST. I sent an order without a total, expecting the handler to run and the order to be saved with a missing field. Fastify never let the handler run: the schema caught the missing field first and answered with a 400 and the message &lt;code&gt;body must have required property 'total'&lt;/code&gt;. That is the schema doing its job, and it is also the moment a beginner usually gets confused, because the handler code looks fine and never executes. The fix is not in the handler at all. Send the missing field, and the same route answers 201 with the new order, id 104, as the last block of output shows.&lt;/p&gt;

&lt;p&gt;The server also runs for real on a port. I started it listening on &lt;a href="http://127.0.0.1:4174" rel="noopener noreferrer"&gt;http://127.0.0.1:4174&lt;/a&gt; and called it with curl. A GET for order 103 answered 200 with Sara's notebook set, and the same incomplete POST answered 400 with the identical validation body. The inject tests and the live server agree, which is the point: inject is a fast way to test the same routes the live server serves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, sending a body that fails the schema. The server answers 400 with &lt;code&gt;body must have required property 'total'&lt;/code&gt; and the code &lt;code&gt;FST_ERR_VALIDATION&lt;/code&gt;, and the handler never runs. The fix is in the request, not the handler: send every required field, and read the message, which names the missing one. Check the schema first whenever a POST fails this way.&lt;/p&gt;

&lt;p&gt;Second, forgetting to return anything from a handler. Fastify does not hang and does not complain. It answers 200 with an empty body and a content-length of 0, which looks like success to the client while carrying no data. The fix is to return the object, as in the example above, or call reply.send() with it. If a route answers 200 with nothing in it, check the handler's return first.&lt;/p&gt;

&lt;p&gt;Third, posting JSON without a Content-Type header. Fastify answers 415 with &lt;code&gt;Unsupported Media Type&lt;/code&gt; and the code &lt;code&gt;FST_ERR_CTP_INVALID_MEDIA_TYPE&lt;/code&gt;, because it only parses a body when it knows the media type. The fix is to send &lt;code&gt;Content-Type: application/json&lt;/code&gt; with the request, which curl and inject both do when you pass a JSON payload the usual way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict, after running it
&lt;/h2&gt;

&lt;p&gt;Fastify does what it claims where it counts: the schema catches a bad order before the handler runs, the handlers stay short because they return values instead of managing responses, and the API is testable without starting a server. The cost is a second framework to learn and less ready-made middleware than Express. If your next project is a JSON API with more than a couple of endpoints, build one route in Fastify and compare it with the Express version. If the Fastify version is shorter and the validation errors are clearer, that is your answer. If it is a single endpoint and the team already knows Express, stay where you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fastify documentation, &lt;a href="https://fastify.dev/docs/latest/" rel="noopener noreferrer"&gt;https://fastify.dev/docs/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Node.js documentation, HTTP module, &lt;a href="https://nodejs.org/docs/latest/api/http.html" rel="noopener noreferrer"&gt;https://nodejs.org/docs/latest/api/http.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JSON Schema, Understanding JSON Schema, &lt;a href="https://json-schema.org/understanding-json-schema/" rel="noopener noreferrer"&gt;https://json-schema.org/understanding-json-schema/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/fastify-fast-node-api-without-ceremony.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/fastify-fast-node-api-without-ceremony.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>fastify</category>
    </item>
    <item>
      <title>Zustand: React State Without the Boilerplate</title>
      <dc:creator>The Practical Developer</dc:creator>
      <pubDate>Wed, 11 Nov 2026 04:36:15 +0000</pubDate>
      <link>https://dev.to/practicaldevblog/zustand-react-state-without-the-boilerplate-4a2g</link>
      <guid>https://dev.to/practicaldevblog/zustand-react-state-without-the-boilerplate-4a2g</guid>
      <description>&lt;p&gt;Picture a small shop page with a cart icon in the corner. A visitor adds a tote bag and a mug. Three parts of the page need to know what happened: the cart icon showing the count, the cart drawer listing the items, and the checkout button showing the total. If each part keeps its own copy of the cart, the numbers drift apart. The usual fix is to lift the state up to a shared parent, but that turns every middle component into a courier for data it never uses. That is the problem Zustand sets out to solve, and the question is whether a tiny store does the job better than the tools React already ships with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would check before using it
&lt;/h2&gt;

&lt;p&gt;My first check is how much state needs sharing. One cart icon and one list on the same page can live in a parent component, and a library buys nothing there. Once three or more distant parts read the same state, or it must survive a route change, a shared store earns its keep. Count the readers first.&lt;/p&gt;

&lt;p&gt;My second check is the learning curve, and Zustand is light here. A store is a function call with state fields plus the functions that change them. No provider, no reducer, no action types. Anyone who knows useState can read a Zustand store, and that matters, because the cost of a library is paid whenever someone new opens the file.&lt;/p&gt;

&lt;p&gt;My third check is how components subscribe. With React context, every reader re-renders on any change, even parts it never touches. Zustand lets each component pick just the slice it needs, so the cart icon re-renders on a count change and ignores a price edit. On a small shop that is invisible; on a busy page it is the difference between a snappy interface and one that stutters.&lt;/p&gt;

&lt;p&gt;My fourth check is the escape hatches. Zustand also works outside React, through its vanilla store, so the same cart logic runs in a plain script or a test. That beats context, which only exists inside a React tree.&lt;/p&gt;

&lt;p&gt;My verdict, as judgement: use Zustand when several components share state that changes a lot and you want the simplest tool. Skip it when the state lives in one component, or when you need time travel debugging or a strict action log. The recommendation flips when the shared state shrinks to one parent and child, or the team needs a framework's guardrails rather than a plain object's freedom.&lt;/p&gt;

&lt;h2&gt;
  
  
  One small shop cart, run end to end
&lt;/h2&gt;

&lt;p&gt;The example is a small shop cart built with the vanilla version of Zustand, so it runs in plain Node with no React at all. The store holds an array of cart items and three actions: add an item, remove one by id, and clear the cart. A total function computes the price from the items, so the total can never drift out of sync with the list. A subscriber logs the item count and total after every change, standing in for the parts of a page that would re-render in React.&lt;/p&gt;

&lt;p&gt;Here is the store and the script that drives it. The products are a canvas tote bag at $18, a ceramic mug at $14.50, and a notebook set at $9.25:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;createStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"zustand/vanilla"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Canvas tote bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;18.0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Ceramic mug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;14.5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Notebook set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;9.25&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cartStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;clearCart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Cart changed: ${count} item(s), total $${state.total().toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Adding 2 tote bags and 1 mug..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Adding 3 notebook sets..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Removing the mug..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Final cart:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`- ${item.qty} x ${item.name} @ $${item.price} = $${(item.price * item.qty).toFixed(2)}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Grand total: $"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the real output from running it with Node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Adding 2 tote bags and 1 mug...
Cart changed: 2 item(s), total $36.00
Cart changed: 3 item(s), total $50.50
Adding 3 notebook sets...
Cart changed: 6 item(s), total $78.25
Removing the mug...
Cart changed: 5 item(s), total $63.75
Final cart:
- 2 x Canvas tote bag @ $18 = $36.00
- 3 x Notebook set @ $9.25 = $27.75
Grand total: $63.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are worth noticing. First, the total is always computed from the items, never stored separately, so adding a notebook set moves the total from $50.50 to $78.25 with no extra code. Second, the subscriber fired exactly once per change, which is how a React component would know to re-render. Third, adding the same product twice creates no duplicate line: the second tote bag merged into the existing one by raising the quantity, which is the behaviour a real shop wants.&lt;/p&gt;

&lt;p&gt;The thing that went wrong was a direct push into the state array. Instead of calling the addItem action, I reached into the store and pushed a pen set straight onto the items array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="n"&gt;cartStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Pen set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;6.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading the store afterwards showed 2 items, so the push had worked, but the subscriber never fired. Here is the real output from that run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Items before the bad line: 1
Items after direct push: 2
Subscriber calls after direct push: 0
Adding a notebook set the proper way...
Subscriber fired, call #1
Items now: Canvas tote bag, Pen set, Notebook set
Subscriber calls total: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The push changed the data without telling anyone. In React the cart icon would still show the old count, because no re-render was triggered, and the pen set would only appear after the next real update forced a render. That is a silent bug: nothing throws, nothing looks broken, and the page quietly shows stale numbers. The fix is the rule Zustand is built around: never touch the state object directly, always go through set. The addItem action creates a new array with the spread operator, and that new array is what wakes the subscribers. Keep that rule and the store stays honest; break it and the store lies to the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes and how to fix them
&lt;/h2&gt;

&lt;p&gt;First, mutating state directly, as in the broken run above. The symptom is a store holding new data while the page shows old numbers, with no error anywhere to point at. The fix is to always change state through set with a new array or object, using the spread operator or map and filter, and never push, splice, or assign onto state from getState.&lt;/p&gt;

&lt;p&gt;Second, subscribing to the whole store. A component that reads the entire store re-renders on every change, even one it never displays, so a price edit re-renders the cart icon. The fix is to select only the slice it needs, for example just the item count or just the total, and let the rest of the store change without disturbing it.&lt;/p&gt;

&lt;p&gt;Third, holding a stale snapshot. Calling getState once, saving the result, and reading it later gives you the state as it was at that moment, not as it is now, so a total computed from it stays frozen while the cart keeps changing. The fix is to call getState fresh inside the action that needs it, or to subscribe, so the value you read is always current.&lt;/p&gt;

&lt;h2&gt;
  
  
  My call on Zustand
&lt;/h2&gt;

&lt;p&gt;Zustand does one job and stays out of the way: it holds shared state in an object and tells the components that care when it changes. The cart above needed a short store, a computed total, and a subscriber, and nothing in it was new. That is the whole appeal, and also the limit: dozens of tangled stores can turn that freedom into a hunt for which action changed what. For a shop cart, a theme toggle, or a dashboard, it is the right size. Try it on one shared piece of state and see whether the code gets shorter. If it does, keep going; if the state was never shared, a store was never needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zustand documentation, &lt;a href="https://zustand.docs.pmnd.rs/" rel="noopener noreferrer"&gt;https://zustand.docs.pmnd.rs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React docs, Managing State, &lt;a href="https://react.dev/learn/managing-state" rel="noopener noreferrer"&gt;https://react.dev/learn/managing-state&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React docs, Sharing State Between Components, &lt;a href="https://react.dev/learn/sharing-state-between-components" rel="noopener noreferrer"&gt;https://react.dev/learn/sharing-state-between-components&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at &lt;a href="https://practicaldevblog.blogspot.com/2026/11/zustand-react-state-without-boilerplate.html" rel="noopener noreferrer"&gt;https://practicaldevblog.blogspot.com/2026/11/zustand-react-state-without-boilerplate.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>zustand</category>
    </item>
  </channel>
</rss>
