<?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: Juan Torchia</title>
    <description>The latest articles on DEV Community by Juan Torchia (@jtorchia).</description>
    <link>https://dev.to/jtorchia</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%2F885942%2F099b05dc-1940-49f6-a022-9c6a392bb405.jpg</url>
      <title>DEV Community: Juan Torchia</title>
      <link>https://dev.to/jtorchia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jtorchia"/>
    <language>en</language>
    <item>
      <title>The Complete Guide to Docker HEALTHCHECK: Dockerfile vs Compose vs Orchestrator</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:00:17 +0000</pubDate>
      <link>https://dev.to/jtorchia/the-complete-guide-to-docker-healthcheck-dockerfile-vs-compose-vs-orchestrator-1j5i</link>
      <guid>https://dev.to/jtorchia/the-complete-guide-to-docker-healthcheck-dockerfile-vs-compose-vs-orchestrator-1j5i</guid>
      <description>&lt;p&gt;How many times have you seen a &lt;code&gt;HEALTHCHECK CMD curl -f http://localhost/health || exit 1&lt;/code&gt; pasted into a Dockerfile without anyone asking what happens when that endpoint returns 200 while the database behind it is dead?&lt;/p&gt;

&lt;p&gt;That's the scene that triggered this post. Not a production incident story — I don't have that kind of public evidence to show here — but a pattern that repeats every time someone searches "docker healthcheck", "dockerfile healthcheck" or "docker container health check" on Google expecting a quick recipe. They get one. And with that recipe, in a real deployment, the orchestrator ends up restarting healthy containers or leaving broken ones running, depending on which side the error is on.&lt;/p&gt;

&lt;p&gt;My thesis is simple and I'll stand by it: &lt;strong&gt;a healthcheck with no criteria behind it — the classic copy-pasted curl to &lt;code&gt;/health&lt;/code&gt; — is infrastructure folklore, not real observability.&lt;/strong&gt; It's good for checking a box on a best-practices checklist. It's useless for knowing whether the container can actually serve traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real pain before you write a single line of HEALTHCHECK
&lt;/h2&gt;

&lt;p&gt;The problem isn't syntax — the docs solve that in two minutes. The problem is deciding &lt;strong&gt;what&lt;/strong&gt; to check, how often, and what to do when the check fails. That's where most guides stop short: they hand you the command and leave you alone with the decision that actually matters.&lt;/p&gt;

&lt;p&gt;And that decision has concrete consequences on a stack running Next.js or Node behind PostgreSQL: a badly placed healthcheck isn't neutral. It generates false positives that restart containers mid-workload, or false negatives that keep sending traffic to a process that can no longer respond with anything useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the official source says (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;Docker's documentation on &lt;code&gt;HEALTHCHECK&lt;/code&gt; is clear on the syntax side. It defines the instruction, its flags (&lt;code&gt;--interval&lt;/code&gt;, &lt;code&gt;--timeout&lt;/code&gt;, &lt;code&gt;--start-period&lt;/code&gt;, &lt;code&gt;--retries&lt;/code&gt;), and the exit codes Docker interprets: &lt;code&gt;0&lt;/code&gt; healthy, &lt;code&gt;1&lt;/code&gt; unhealthy, &lt;code&gt;2&lt;/code&gt; reserved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Official Dockerfile syntax&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=5s --start-period=10s --retries=3 \&lt;/span&gt;
  CMD curl -f http://localhost:3000/health || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what the official source gives you: &lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/dockerfile/#healthcheck&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What it &lt;strong&gt;doesn't&lt;/strong&gt; give you — and that's the whole point of this post — is judgment about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What that &lt;code&gt;/health&lt;/code&gt; endpoint should actually return to be honest ("the process is alive" vs "I can talk to the database")&lt;/li&gt;
&lt;li&gt;What interval makes sense for your real load&lt;/li&gt;
&lt;li&gt;What happens when the orchestrator (Swarm, Kubernetes, Railway) decides what to do with an "unhealthy" container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs give you the tool. They don't give you the signal design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile vs Compose vs orchestrator: not the same question
&lt;/h2&gt;

&lt;p&gt;Here's where the confusion from the three searches in the title collides. These are three distinct layers, and each answers a different question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Dockerfile HEALTHCHECK] --&amp;gt;|defines the test| B[Docker Engine]
  B --&amp;gt;|marks status| C{Compose depends_on: condition}
  C --&amp;gt;|healthy| D[Starts the next service]
  C --&amp;gt;|unhealthy| E[Blocks or retries]
  B --&amp;gt; F{Orchestrator: Swarm/K8s}
  F --&amp;gt;|repeated unhealthy| G[Replaces the container]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; defines the test itself: the command, the interval, the retries. It's the lowest layer, it lives with the image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose&lt;/strong&gt; consumes that status to sequence startup with &lt;code&gt;depends_on: condition: service_healthy&lt;/code&gt;, or to override the HEALTHCHECK parameters without touching the image:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The orchestrator&lt;/strong&gt; (Swarm, Kubernetes with its own liveness/readiness probes, or a platform like Railway) decides what to do with that signal: retry, replace the container, pull it out of the load balancer. At that point Docker's HEALTHCHECK stops being the only source of truth — Kubernetes, for instance, has its own probes that don't depend on the Dockerfile's HEALTHCHECK at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing up these three layers is exactly why someone searches "docker healthcheck" thinking there's one answer, when really they're asking three different things depending on which layer they're standing in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get it wrong: the common recipe and its hidden cost
&lt;/h2&gt;

&lt;p&gt;The common recipe is this: expose a &lt;code&gt;/health&lt;/code&gt; endpoint that returns &lt;code&gt;200 OK&lt;/code&gt; with a hardcoded &lt;code&gt;{"status": "ok"}&lt;/code&gt;, and don't touch anything else. It compiles, it works in the demo, it checks the "I have a healthcheck" box.&lt;/p&gt;

&lt;p&gt;The hidden cost shows up when that Node process is still alive — the runtime responds, the port is listening — but the PostgreSQL connection dropped, the connection pool is exhausted, or a critical external dependency isn't responding. The healthcheck says "healthy." The container can't serve a single real request.&lt;/p&gt;

&lt;p&gt;It's the same design mistake I ran into when I talked about &lt;a href="https://juanchi.dev/en/blog/strict-null-checks-typescript-production-failures" rel="noopener noreferrer"&gt;what to expose and what to hide in Actuator&lt;/a&gt;: the surface you decide to show as "status" has to reflect what actually matters, not what's easy to check. A &lt;code&gt;/health&lt;/code&gt; that only confirms the process started is equivalent to an Actuator endpoint that returns &lt;code&gt;UP&lt;/code&gt; without checking any real dependency.&lt;/p&gt;

&lt;p&gt;The honest counterexample, and the one I actually worry about more: an overly strict healthcheck can do just as much damage. Picture an endpoint that checks the database, the cache, and three external services on every 10-second ping. As a rule of thumb — not something I've measured in production, but a pattern that shows up constantly in infra discussions — any transient latency spike in one of those external dependencies is enough to drag the whole container down to "unhealthy." The orchestrator restarts it, killing active connections, over a problem that most likely would've resolved itself on the next retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix: what to look at before writing the CMD
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What to check&lt;/th&gt;
&lt;th&gt;Suggested interval&lt;/th&gt;
&lt;th&gt;Risk if you get it wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple stateless API&lt;/td&gt;
&lt;td&gt;Process responds on the port&lt;/td&gt;
&lt;td&gt;30s, timeout 5s&lt;/td&gt;
&lt;td&gt;Low — not much to break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API with PostgreSQL connection&lt;/td&gt;
&lt;td&gt;Port + lightweight query like &lt;code&gt;SELECT 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;15-30s, high retries (3-5)&lt;/td&gt;
&lt;td&gt;High if the check is heavy: overloads the database with pings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker with no HTTP port&lt;/td&gt;
&lt;td&gt;Lock file, processed queue, own heartbeat&lt;/td&gt;
&lt;td&gt;Depends on the job cycle&lt;/td&gt;
&lt;td&gt;False "unhealthy" if the cycle runs longer than the interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service behind Compose with &lt;code&gt;depends_on&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Make sure &lt;code&gt;service_healthy&lt;/code&gt; doesn't block the whole stack's startup indefinitely&lt;/td&gt;
&lt;td&gt;Generous &lt;code&gt;start_period&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Whole stack fails to start over a too-short &lt;code&gt;start_period&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container in an orchestrator (Swarm/K8s)&lt;/td&gt;
&lt;td&gt;Separate liveness (is it alive?) from readiness (can it take traffic?)&lt;/td&gt;
&lt;td&gt;Relaxed liveness, strict readiness&lt;/td&gt;
&lt;td&gt;Cascading restarts if liveness and readiness share the same check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matrix isn't a closed formula. It's a starting point to ask "is what I'm checking actually what fails when the service fails?" before copying the first example you find in a tutorial.&lt;/p&gt;

&lt;p&gt;I use a similar filter for npm libraries: before putting a dependency into production, it's worth &lt;a href="https://juanchi.dev/en/blog/npm-dependencies-how-to-evaluate-before-production" rel="noopener noreferrer"&gt;evaluating it with actual criteria&lt;/a&gt; instead of adding it because "everyone uses it." Same logic applies to a HEALTHCHECK — the fact that a command shows up in a hundred GitHub Dockerfiles says nothing about whether it fits your case. Popularity isn't evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes / gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;curl&lt;/code&gt; without having it in the final image.&lt;/strong&gt; If the Dockerfile uses a slim or alpine base, &lt;code&gt;curl&lt;/code&gt; might not be installed, and the healthcheck fails every single time with a "command not found" error, not because of an actual service problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;start_period&lt;/code&gt; too short for apps with slow startup.&lt;/strong&gt; If the app takes 15 seconds to come up (migrations, pool connection, warm-up) and &lt;code&gt;start_period&lt;/code&gt; is set to 5 seconds, the container gets marked unhealthy before it's even finished booting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing liveness with readiness.&lt;/strong&gt; A check that only confirms "the process hasn't crashed" doesn't tell you if it can serve traffic. That distinction, which Kubernetes makes explicit with two separate probes, gets lost easily when plain Docker only gives you one &lt;code&gt;HEALTHCHECK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthchecks that write to the database just to verify.&lt;/strong&gt; A check that does a test &lt;code&gt;INSERT&lt;/code&gt; every 10 seconds generates noise in the PostgreSQL logs — something you notice fast if you've ever turned on &lt;a href="https://juanchi.dev/en/blog/prisma-query-logging-postgresql-orm-limits" rel="noopener noreferrer"&gt;Prisma's query logging&lt;/a&gt; and watched that background traffic compete with the real queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not logging the healthcheck result.&lt;/strong&gt; &lt;code&gt;docker inspect --format='{{json .State.Health}}' &amp;lt;container&amp;gt;&lt;/code&gt; gives you the history of the last checks. If you've never looked at it, it's hard to know whether the healthcheck is actually doing anything or just sitting there for decoration.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check the health check history of a running container&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{json .State.Health}}'&lt;/span&gt; mi_contenedor | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limits of this guide
&lt;/h2&gt;

&lt;p&gt;This is design judgment based on the official docs and known failure patterns, not an experiment with my own metrics. I don't have a reproducible benchmark comparing intervals, or a documented public production case to cite here. If you're deciding on the healthcheck for a system with a real SLA, the next step isn't reading a blog post — it's instrumenting your own system, running a controlled-load experiment, and watching the &lt;code&gt;docker inspect&lt;/code&gt; logs over a representative period. This guide gives you the framework to design that test, not the result of having run it.&lt;/p&gt;

&lt;p&gt;There's also no evidence here about specific Kubernetes probe behavior or Railway — every orchestrator has its own semantics, and it's worth reading its specific docs before assuming it behaves the same as Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between HEALTHCHECK in Dockerfile vs Compose?&lt;/strong&gt;&lt;br&gt;
The Dockerfile defines the image's default healthcheck. Compose can inherit it or override it with its own &lt;code&gt;healthcheck&lt;/code&gt; section, without rebuilding the image. Useful for tuning intervals per environment (dev vs staging) without touching the Dockerfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if I don't set any HEALTHCHECK?&lt;/strong&gt;&lt;br&gt;
Docker assumes the container is healthy as long as the main process keeps running. No active check. That's not necessarily worse than a badly designed healthcheck — sometimes "no check" is more honest than a check that lies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a good interval for HEALTHCHECK?&lt;/strong&gt;&lt;br&gt;
There's no universal number. It depends on how expensive the check is and how fast you need to detect a problem. A typical reference range is 10-30 seconds with a short &lt;code&gt;timeout&lt;/code&gt; (3-5s) and &lt;code&gt;retries&lt;/code&gt; of 3 to 5 to avoid false positives from a transient spike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does HEALTHCHECK replace Kubernetes probes?&lt;/strong&gt;&lt;br&gt;
No. Kubernetes has its own &lt;code&gt;livenessProbe&lt;/code&gt; and &lt;code&gt;readinessProbe&lt;/code&gt;, independent of Docker's &lt;code&gt;HEALTHCHECK&lt;/code&gt;. If you deploy on K8s, the Dockerfile's HEALTHCHECK might end up unused — the real config lives in the pod manifest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use a script instead of curl?&lt;/strong&gt;&lt;br&gt;
Yes. HEALTHCHECK's &lt;code&gt;CMD&lt;/code&gt; accepts any command that returns an exit code of 0 or non-zero. A custom script gives you more control to check, for example, whether the PostgreSQL connection pool has available connections, instead of just hitting an HTTP port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a too-strict healthcheck backfire?&lt;/strong&gt;&lt;br&gt;
Yes, and it's one of the central points of this guide. If the check depends on external services with variable latency, a transient spike can drag the container into "unhealthy" and trigger a restart that fixes nothing — because the real problem was outside the container, not inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: the position
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;HEALTHCHECK&lt;/code&gt; isn't a best-practices checkbox. It's a signal that some other system — Compose, Swarm, Kubernetes, whatever platform you're using — is going to use to make an automatic decision about your container. Designing it without thinking about what decision you're going to trigger is folklore, not observability.&lt;/p&gt;

&lt;p&gt;My concrete recommendation: before writing the &lt;code&gt;CMD&lt;/code&gt;, write down the question that check has to answer first — "can this process serve traffic right now?" — and only then write the command. If the answer needs a &lt;code&gt;SELECT 1&lt;/code&gt; against PostgreSQL, let it have one. If it needs to separate liveness from readiness because the process can be alive but not ready, let it separate them. The command is the easy part. Deciding what to ask is what makes the healthcheck useful on the day something actually breaks. The uncomfortable question worth sitting with: if your healthcheck failed right now, would it be telling you the truth, or just following a script nobody re-checked?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt; Docker Docs - HEALTHCHECK — &lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/dockerfile/#healthcheck&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/docker-healthcheck-dockerfile-vs-compose-vs-orchestrator" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>docker</category>
      <category>devops</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Guía completa de HEALTHCHECK en Docker: Dockerfile vs Compose vs orquestador</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 04 Aug 2026 12:00:12 +0000</pubDate>
      <link>https://dev.to/jtorchia/guia-completa-de-healthcheck-en-docker-dockerfile-vs-compose-vs-orquestador-1oj8</link>
      <guid>https://dev.to/jtorchia/guia-completa-de-healthcheck-en-docker-dockerfile-vs-compose-vs-orquestador-1oj8</guid>
      <description>&lt;p&gt;¿Cuántas veces viste un &lt;code&gt;HEALTHCHECK CMD curl -f http://localhost/health || exit 1&lt;/code&gt; pegado en un Dockerfile sin que nadie se preguntara qué pasa si ese endpoint responde 200 con la base de datos caída atrás?&lt;/p&gt;

&lt;p&gt;Esa es la escena que motiva este post. No una anécdota de incidente en producción — no tengo esa evidencia pública para mostrar acá — sino un patrón que se repite cada vez que alguien busca "docker healthcheck", "dockerfile healthcheck" o "docker container health check" en Google esperando una receta rápida. La reciben. Y con esa receta, en un despliegue real, el orquestador reinicia contenedores sanos o deja corriendo contenedores rotos, según de qué lado esté el error.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple y la sostengo: &lt;strong&gt;un healthcheck sin criterio, el clásico curl a &lt;code&gt;/health&lt;/code&gt; copiado y pegado, es folklore de infraestructura, no observabilidad real.&lt;/strong&gt; Sirve para completar un checklist de buenas prácticas. No sirve para saber si el contenedor puede atender tráfico.&lt;/p&gt;

&lt;h2&gt;
  
  
  El dolor real antes de escribir una línea de HEALTHCHECK
&lt;/h2&gt;

&lt;p&gt;El problema no es la sintaxis — eso lo resuelve la documentación en dos minutos. El problema es decidir &lt;strong&gt;qué&lt;/strong&gt; chequear, con qué frecuencia, y qué hacer cuando el chequeo falla. Ahí es donde la mayoría de las guías se quedan cortas: te dan el comando y te dejan solo con la decisión que realmente importa.&lt;/p&gt;

&lt;p&gt;Y esa decisión tiene consecuencias concretas en un stack con Next.js o Node corriendo detrás de PostgreSQL: un healthcheck mal puesto no es neutral. Genera falsos positivos que reinician contenedores en medio de una carga de trabajo normal, o falsos negativos que dejan tráfico yendo a un proceso que ya no puede responder nada útil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué dice la fuente oficial (y qué no dice)
&lt;/h2&gt;

&lt;p&gt;La documentación de Docker sobre &lt;code&gt;HEALTHCHECK&lt;/code&gt; es clara en lo sintáctico. Define la instrucción, sus flags (&lt;code&gt;--interval&lt;/code&gt;, &lt;code&gt;--timeout&lt;/code&gt;, &lt;code&gt;--start-period&lt;/code&gt;, &lt;code&gt;--retries&lt;/code&gt;) y los códigos de salida que Docker interpreta: &lt;code&gt;0&lt;/code&gt; sano, &lt;code&gt;1&lt;/code&gt; no sano, &lt;code&gt;2&lt;/code&gt; reservado.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sintaxis oficial de Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=5s --start-period=10s --retries=3 \&lt;/span&gt;
  CMD curl -f http://localhost:3000/health || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es lo que la fuente oficial te da: &lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/dockerfile/#healthcheck&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lo que &lt;strong&gt;no&lt;/strong&gt; te da — y ahí está el punto de este post — es criterio sobre:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qué debería devolver ese endpoint &lt;code&gt;/health&lt;/code&gt; para ser honesto (¿solo "el proceso está vivo" o "puedo hablar con la base"?)&lt;/li&gt;
&lt;li&gt;Qué intervalo tiene sentido para tu carga real&lt;/li&gt;
&lt;li&gt;Qué pasa cuando el orquestador (Swarm, Kubernetes, Railway) decide qué hacer con un contenedor "unhealthy"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La documentación te da la herramienta. No te da el diseño de la señal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile vs Compose vs orquestador: no es la misma pregunta
&lt;/h2&gt;

&lt;p&gt;Acá está la confusión que junta las tres búsquedas del título. Son tres capas distintas y cada una responde una pregunta distinta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Dockerfile HEALTHCHECK] --&amp;gt;|define la prueba| B[Docker Engine]
  B --&amp;gt;|marca estado| C{Compose depends_on: condition}
  C --&amp;gt;|healthy| D[Levanta el siguiente servicio]
  C --&amp;gt;|unhealthy| E[Bloquea o reintenta]
  B --&amp;gt; F{Orquestador: Swarm/K8s}
  F --&amp;gt;|unhealthy repetido| G[Reemplaza el contenedor]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; define la prueba en sí: el comando, el intervalo, los reintentos. Es la capa más baja, vive con la imagen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose&lt;/strong&gt; consume ese estado para ordenar el arranque con &lt;code&gt;depends_on: condition: service_healthy&lt;/code&gt;, o para overridear los parámetros del HEALTHCHECK sin tocar la imagen:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;20s&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El orquestador&lt;/strong&gt; (Swarm, Kubernetes con sus propios liveness/readiness probes, o una plataforma como Railway) decide qué hacer con esa señal: reintentar, reemplazar el contenedor, sacarlo del balanceo. Ahí el HEALTHCHECK de Docker deja de ser la única fuente de verdad — Kubernetes, por ejemplo, tiene sus propios probes que no dependen del HEALTHCHECK del Dockerfile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Confundir estas tres capas es la razón por la que alguien busca "docker healthcheck" pensando que hay una sola respuesta, cuando en realidad está preguntando tres cosas distintas según en qué capa esté parado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente: la receta común y su costo oculto
&lt;/h2&gt;

&lt;p&gt;La receta común es esta: exponer un endpoint &lt;code&gt;/health&lt;/code&gt; que devuelve &lt;code&gt;200 OK&lt;/code&gt; con un &lt;code&gt;{"status": "ok"}&lt;/code&gt; hardcodeado, sin tocar nada más. Compila, funciona en el demo, pasa el checklist de "tengo healthcheck".&lt;/p&gt;

&lt;p&gt;El costo oculto aparece cuando ese proceso Node sigue vivo — el runtime responde, el puerto escucha — pero la conexión a PostgreSQL se cayó, el pool de conexiones está agotado, o una dependencia externa crítica no responde. El healthcheck dice "sano". El contenedor no puede atender una sola request real.&lt;/p&gt;

&lt;p&gt;Es el mismo error de diseño que discutí cuando hablé de &lt;a href="https://juanchi.dev/es/blog/strict-null-checks-typescript-produccion" rel="noopener noreferrer"&gt;qué exponer y qué ocultar en Actuator&lt;/a&gt;: la superficie que decidís mostrar como "estado" tiene que reflejar lo que de verdad importa, no lo que es fácil de chequear. Un &lt;code&gt;/health&lt;/code&gt; que solo confirma que el proceso arrancó es equivalente a un endpoint de Actuator que devuelve &lt;code&gt;UP&lt;/code&gt; sin chequear ninguna dependencia real.&lt;/p&gt;

&lt;p&gt;El contraejemplo honesto, y este es el que más se pasa por alto: un healthcheck demasiado estricto puede ser igual de dañino que uno vacío. Pensalo así — si el endpoint chequea la base, el caché y tres servicios externos en cada ping cada pocos segundos, alcanza con que una sola de esas dependencias tenga una latencia transitoria para que el chequeo completo falle. El orquestador ve "unhealthy" y reinicia el contenedor, cortando conexiones activas por un problema que capaz se resolvía solo en el próximo intento. No tengo un caso productivo propio con logs para mostrar acá, pero es un patrón de fallo conocido en cualquier chequeo que agrega dependencias externas sin un timeout ajustado y sin distinguir "no puedo responder" de "una cosa que consulto está lenta".&lt;/p&gt;

&lt;h2&gt;
  
  
  Matriz de decisión: qué mirar antes de escribir el CMD
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;Qué chequear&lt;/th&gt;
&lt;th&gt;Intervalo sugerido&lt;/th&gt;
&lt;th&gt;Riesgo si te equivocás&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API stateless simple&lt;/td&gt;
&lt;td&gt;Proceso responde en el puerto&lt;/td&gt;
&lt;td&gt;30s, timeout 5s&lt;/td&gt;
&lt;td&gt;Bajo — poco que romper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API con conexión a PostgreSQL&lt;/td&gt;
&lt;td&gt;Puerto + query liviana tipo &lt;code&gt;SELECT 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;15-30s, retries altos (3-5)&lt;/td&gt;
&lt;td&gt;Alto si el chequeo es pesado: sobrecarga la base con pings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker sin puerto HTTP&lt;/td&gt;
&lt;td&gt;Archivo de lock, cola procesada, heartbeat propio&lt;/td&gt;
&lt;td&gt;Depende del ciclo del job&lt;/td&gt;
&lt;td&gt;Falso "unhealthy" si el ciclo es más largo que el intervalo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servicio detrás de Compose con &lt;code&gt;depends_on&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Que el &lt;code&gt;service_healthy&lt;/code&gt; no bloquee el arranque de todo el stack indefinidamente&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start_period&lt;/code&gt; generoso&lt;/td&gt;
&lt;td&gt;Stack entero no levanta por un &lt;code&gt;start_period&lt;/code&gt; corto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contenedor en orquestador (Swarm/K8s)&lt;/td&gt;
&lt;td&gt;Separar liveness (¿está vivo?) de readiness (¿puede recibir tráfico?)&lt;/td&gt;
&lt;td&gt;Liveness relajado, readiness estricto&lt;/td&gt;
&lt;td&gt;Reinicios en cascada si liveness y readiness comparten el mismo chequeo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Esta matriz no es una fórmula cerrada. Es un punto de partida para preguntarte "¿esto que estoy chequeando es lo que realmente falla cuando el servicio falla?" antes de copiar el primer ejemplo que aparece en un tutorial.&lt;/p&gt;

&lt;p&gt;Ya me pasó algo parecido con librerías npm: la tentación de sumar una dependencia porque "todos la usan" en vez de &lt;a href="https://juanchi.dev/es/blog/evaluar-dependencias-npm-seguridad-mantenimiento-2" rel="noopener noreferrer"&gt;evaluarla con criterio propio&lt;/a&gt;. Con un HEALTHCHECK el vicio es idéntico — que el comando aparezca copiado en cien Dockerfiles de GitHub no dice absolutamente nada sobre si tiene sentido para tu caso puntual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes / gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usar &lt;code&gt;curl&lt;/code&gt; sin tenerlo en la imagen final.&lt;/strong&gt; Si el Dockerfile usa una imagen slim o alpine, &lt;code&gt;curl&lt;/code&gt; puede no estar instalado y el healthcheck falla siempre por un error de "command not found", no por un problema real del servicio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;start_period&lt;/code&gt; demasiado corto para apps con arranque lento.&lt;/strong&gt; Si la app tarda 15 segundos en levantar (migraciones, conexión a pool, warm-up) y el &lt;code&gt;start_period&lt;/code&gt; es de 5 segundos, el contenedor se marca unhealthy antes de terminar de arrancar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confundir liveness con readiness.&lt;/strong&gt; Un chequeo que solo confirma "el proceso no crasheó" no dice si puede atender tráfico. Esa distinción, que Kubernetes hace explícita con dos probes separados, se pierde fácil cuando en Docker plano solo hay un &lt;code&gt;HEALTHCHECK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthchecks que escriben en la base para verificar.&lt;/strong&gt; Un chequeo que hace un &lt;code&gt;INSERT&lt;/code&gt; de prueba cada 10 segundos genera ruido en los logs de PostgreSQL — algo que se nota rápido si alguna vez activaste el &lt;a href="https://juanchi.dev/es/blog/prisma-query-logging-postgresql-limites" rel="noopener noreferrer"&gt;query logging de Prisma&lt;/a&gt; y viste ese tráfico de fondo compitiendo con las queries reales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No loguear el resultado del healthcheck.&lt;/strong&gt; &lt;code&gt;docker inspect --format='{{json .State.Health}}' &amp;lt;container&amp;gt;&lt;/code&gt; te da el historial de los últimos chequeos. Si nunca lo miraste, es difícil saber si el healthcheck está funcionando o solo está ahí de adorno.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ver el historial de health checks de un contenedor corriendo&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{json .State.Health}}'&lt;/span&gt; mi_contenedor | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Límites de esta guía
&lt;/h2&gt;

&lt;p&gt;Esto es criterio de diseño basado en la documentación oficial y en patrones de fallo conocidos, no un experimento con métricas propias. No tengo un benchmark reproducible que compare intervalos ni un caso productivo documentado públicamente para citar acá. Si estás decidiendo el healthcheck de un sistema con SLA real, el próximo paso no es leer un blog — es instrumentar tu propio sistema, correr un experimento con carga controlada y mirar los logs de &lt;code&gt;docker inspect&lt;/code&gt; durante un período representativo. Esta guía te da el marco para diseñar esa prueba, no el resultado de haberla corrido.&lt;/p&gt;

&lt;p&gt;Tampoco hay evidencia acá sobre comportamiento específico de Kubernetes probes o Railway — cada orquestador tiene su propia semántica y vale la pena leer su documentación puntual antes de asumir que se comporta igual que Docker Compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es la diferencia entre HEALTHCHECK en Dockerfile y en Compose?&lt;/strong&gt;&lt;br&gt;
El Dockerfile define el healthcheck por defecto de la imagen. Compose puede heredarlo o sobreescribirlo con su propia sección &lt;code&gt;healthcheck&lt;/code&gt;, sin necesidad de reconstruir la imagen. Es útil para ajustar intervalos según el entorno (dev vs staging) sin tocar el Dockerfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa si no pongo ningún HEALTHCHECK?&lt;/strong&gt;&lt;br&gt;
Docker asume que el contenedor está sano mientras el proceso principal siga corriendo. No hay chequeo activo. No es necesariamente peor que un healthcheck mal diseñado — a veces "sin chequeo" es más honesto que un chequeo que miente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es un buen intervalo para HEALTHCHECK?&lt;/strong&gt;&lt;br&gt;
No hay un número universal. Depende de qué tan caro es el chequeo y qué tan rápido necesitás detectar un problema. Un rango típico de referencia es 10-30 segundos con &lt;code&gt;timeout&lt;/code&gt; corto (3-5s) y &lt;code&gt;retries&lt;/code&gt; de 3 a 5 para evitar falsos positivos por un pico transitorio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿HEALTHCHECK reemplaza a los probes de Kubernetes?&lt;/strong&gt;&lt;br&gt;
No. Kubernetes tiene sus propios &lt;code&gt;livenessProbe&lt;/code&gt; y &lt;code&gt;readinessProbe&lt;/code&gt;, independientes del &lt;code&gt;HEALTHCHECK&lt;/code&gt; de Docker. Si desplegás en K8s, el HEALTHCHECK del Dockerfile puede quedar sin uso — la configuración real vive en el manifiesto del pod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar un script en vez de curl?&lt;/strong&gt;&lt;br&gt;
Sí. El &lt;code&gt;CMD&lt;/code&gt; de HEALTHCHECK acepta cualquier comando que devuelva código de salida 0 o distinto de 0. Un script propio te da más control para chequear, por ejemplo, si el pool de conexiones a PostgreSQL tiene conexiones disponibles, en vez de solo golpear un puerto HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Un healthcheck muy estricto puede ser contraproducente?&lt;/strong&gt;&lt;br&gt;
Sí, y es uno de los puntos centrales de esta guía. Si el chequeo depende de servicios externos con latencia variable, un pico transitorio puede tirar el contenedor a "unhealthy" y gatillar un reinicio que no resuelve nada — porque el problema real estaba afuera del contenedor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre: la postura
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;HEALTHCHECK&lt;/code&gt; no es un checkbox de buenas prácticas. Es una señal que otro sistema — Compose, Swarm, Kubernetes, la plataforma que uses — va a usar para tomar una decisión automática sobre tu contenedor. Diseñarlo sin pensar qué decisión vas a gatillar es folklore, no observabilidad.&lt;/p&gt;

&lt;p&gt;Mi recomendación concreta: antes de escribir el &lt;code&gt;CMD&lt;/code&gt;, escribí primero la pregunta que ese chequeo tiene que responder — "¿puede este proceso atender tráfico ahora mismo?" — y recién después el comando. Si la respuesta necesita un &lt;code&gt;SELECT 1&lt;/code&gt; a PostgreSQL, que lo tenga. Si necesita separar liveness de readiness porque el proceso puede estar vivo pero no listo, que lo separe. El comando es la parte fácil. La decisión de qué preguntar es la que hace que el healthcheck sirva para algo el día que algo se rompe de verdad.&lt;/p&gt;

&lt;p&gt;La pregunta incómoda que me queda dando vueltas: ¿tu healthcheck actual lo diseñaste pensando en esto, o lo copiaste de un ejemplo y nunca lo volviste a mirar?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt; Docker Docs - HEALTHCHECK — &lt;a href="https://docs.docker.com/reference/dockerfile/#healthcheck" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/dockerfile/#healthcheck&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/dockerfile-healthcheck-guia-completa" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Qwen3 locally with Ollama: what changed in the architecture and whether it's worth switching</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:00:16 +0000</pubDate>
      <link>https://dev.to/jtorchia/qwen3-locally-with-ollama-what-changed-in-the-architecture-and-whether-its-worth-switching-4m8g</link>
      <guid>https://dev.to/jtorchia/qwen3-locally-with-ollama-what-changed-in-the-architecture-and-whether-its-worth-switching-4m8g</guid>
      <description>&lt;h1&gt;
  
  
  Qwen3 locally with Ollama: what changed in the architecture and whether it's worth switching
&lt;/h1&gt;

&lt;p&gt;In 2005, when I was 16 and managing the cyber café, I learned something I still apply today: don't change what works until you can prove the new thing beats it &lt;em&gt;in your&lt;/em&gt; scenario. Not in someone else's benchmark. Not in the vendor announcement. In yours. Every time we updated something without a clear reason, someone ended up diagnosing a connection outage at 11pm with a full house.&lt;/p&gt;

&lt;p&gt;I see the exact same thing every time a new model drops. Qwen3 comes out, Twitter explodes, and the question nobody asks is the only one that matters: &lt;strong&gt;is it actually worth replacing the model you already have running in Ollama, or is this more hype than substance?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My thesis: Qwen3 is genuinely interesting for local inference, but the interesting part isn't the model itself — it's that most teams evaluate the switch backwards. They test the new model in isolation, like it, and only discover the real cost (parser breaking on &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tokens, context window assumptions that don't hold, sampling defaults that don't transfer) once it's already in the pipeline. The thinking mode support and the code quality improvements are real, documented by the Alibaba team themselves. But for most agent pipelines running Llama 3.1 or 3.2, the jump doesn't justify blowing up your setup overnight. You need to measure first, and you need to measure the failure modes, not just the wins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qwen3 in Ollama: what the architecture actually says (and what it doesn't)
&lt;/h2&gt;

&lt;p&gt;Qwen3 is available in Ollama (&lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;ollama.com/library/qwen3&lt;/a&gt;) in multiple sizes: 0.6B, 1.7B, 4B, 8B, 14B, 30B-A3B (MoE), 32B, and 235B-A22B (MoE). That's already a signal — the Qwen team isn't only targeting peak performance, they're covering the range of models a developer can actually run on reasonable hardware.&lt;/p&gt;

&lt;p&gt;What the &lt;a href="https://qwenlm.github.io/blog/qwen3/" rel="noopener noreferrer"&gt;official Qwen blog&lt;/a&gt; and the &lt;a href="https://huggingface.co/Qwen/Qwen3-8B" rel="noopener noreferrer"&gt;Hugging Face model card&lt;/a&gt; document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt-activatable thinking mode&lt;/strong&gt;: Qwen3 supports explicit reasoning (chain of thought) that can be turned on or off depending on the use case. That's useful in pipelines where you need reasoning traceability without having to maintain two separate models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MoE variants&lt;/strong&gt; (Mixture of Experts): the 30B-A3B and 235B-A22B models activate only a fraction of their parameters per inference. On paper, that reduces the computational cost relative to the total model size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extended multilingual support&lt;/strong&gt;: the team claims support for 119 languages, including Spanish. Relevant for pipelines that need to work in languages other than English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improvements in reasoning and code&lt;/strong&gt;: comparisons published by the Qwen team show solid results in code and math benchmarks against previous-generation models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What that evidence doesn't say&lt;/strong&gt;: the published benchmarks are the ones the team selected. I don't have my own production logs with Qwen3, and I'm not going to fabricate them. What I can do is help you reason through the switch with reproducible technical criteria.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where people go wrong when adopting a new model
&lt;/h2&gt;

&lt;p&gt;The most common mistake isn't technical — it's a judgment failure. The pattern I keep seeing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new model drops with flashy benchmarks.&lt;/li&gt;
&lt;li&gt;Someone tests it with a single prompt and it "works great."&lt;/li&gt;
&lt;li&gt;They drop it into the pipeline with no clear baseline.&lt;/li&gt;
&lt;li&gt;Days later, something downstream starts behaving oddly — a parser choking on unexpected tokens, an output that used to be short suddenly padded with reasoning traces — and nobody can say for sure whether it's the model, the context, or both. This is the failure mode I'd flag as the one to actually test for, not something I've logged myself with Qwen3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a TypeScript agent pipeline running on Ollama, the hidden cost of switching models is higher than it looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output format changes&lt;/strong&gt;: Qwen3 can generate &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; tokens when thinking mode is active. If your agent's parser isn't expecting that block, it's going to break the JSON or text that downstream consumers rely on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different context window&lt;/strong&gt;: Qwen3-8B declares a 128K token context window per the model card. If the pipeline assumes a smaller limit, it can behave differently in subtle ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature and sampling&lt;/strong&gt;: every model has a different sampling space. What worked with Llama 3.1 at &lt;code&gt;temperature: 0.7&lt;/code&gt; doesn't transfer directly.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A basic checkpoint before migrating models in an Ollama pipeline&lt;/span&gt;
&lt;span class="c1"&gt;// Not a guarantee — a minimum-friction checklist&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qwen3:8b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Disable thinking mode if you don't need explicit traceability&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;num_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Start conservative — don't assume 128K is free in RAM&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// If the pipeline parses structured JSON, add validation for &amp;lt;think&amp;gt; blocks&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Before deploying: run the same prompt set with the previous model&lt;/span&gt;
&lt;span class="c1"&gt;// and with Qwen3, then compare outputs. No baseline, no decision.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thinking mode in Qwen3 is real and useful, but it requires the pipeline to handle it explicitly. If you don't, you're paying the cost of extra tokens without capturing the benefit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision matrix: when does switching to Qwen3 actually make sense
&lt;/h2&gt;

&lt;p&gt;This is the tool I find most useful when evaluating a model change. It's not my own production evidence — it's prudent technical judgment based on what public documentation actually lets you claim.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Does Qwen3 add value?&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent that needs traceable reasoning&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, try it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prompt-activatable thinking mode is a real advantage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript/Python code generation pipeline&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes, try it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code improvements are documented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent parsing strict JSON with no validation layer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not yet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tokens can break the parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanish-language pipeline with Llama 3.1 that already works&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Evaluate first&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The jump isn't guaranteed without your own baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware with less than 16GB RAM running the 8B model&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Careful&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;128K context window has a real memory cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MoE use case (30B-A3B) on limited hardware&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Test locally first&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MoE reduces active compute, but total RAM is still high&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The logic behind each row: if thinking mode is relevant to the use case, Qwen3 has a concrete advantage. If the pipeline already works and you don't need that capability, the migration risk outweighs the expected benefit without your own data.&lt;/p&gt;

&lt;p&gt;This connects to something I covered in the post about &lt;a href="https://juanchi.dev/en/blog/nodejs-runtime-that-changed-backend-forever" rel="noopener noreferrer"&gt;Node.js and the event loop&lt;/a&gt;: runtime or model changes get evaluated in context, not in the abstract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest limits: what you can't conclude from this evidence
&lt;/h2&gt;

&lt;p&gt;Before wrapping up, I need to be explicit about what this evidence doesn't let you claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I don't know if Qwen3 is "better" than Llama 3.1/3.2 for your pipeline&lt;/strong&gt;: that depends on the use case, the prompts, the hardware, and how the agent is structured. Published benchmarks are directional, not decisive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I don't know the actual RAM consumption in your setup&lt;/strong&gt;: the 8B model with a large context window can exceed what the technical spec suggests depending on the Ollama backend and OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I don't know if thinking mode will help or hurt&lt;/strong&gt;: in pipelines that expect short, structured outputs, reasoning tokens can be expensive noise. In pipelines where reasoning quality matters more than latency, they can be genuinely valuable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alibaba chose the benchmarks&lt;/strong&gt;: that doesn't invalidate them, but it's a data point to weigh when reading the evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to validate, the reproducible path is: spin up Qwen3 locally with Ollama, run the same prompt set from your pipeline against both the previous model and Qwen3, and compare. Without that, any conclusion is speculation.&lt;/p&gt;

&lt;p&gt;This applies to broader infrastructure decisions too — like when I discussed &lt;a href="https://juanchi.dev/en/blog/spring-boot-actuator-endpoints-security-expose-hide" rel="noopener noreferrer"&gt;what to expose and what to hide in Spring Boot Actuator&lt;/a&gt;: same principle, don't change what you haven't measured.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Qwen3, Ollama, and local inference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I install Qwen3 in Ollama?&lt;/strong&gt;&lt;br&gt;
One command: &lt;code&gt;ollama pull qwen3:8b&lt;/code&gt;. Replace &lt;code&gt;8b&lt;/code&gt; with the size that matches your hardware. Available sizes are listed at &lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;ollama.com/library/qwen3&lt;/a&gt;. For the 8B you need at least 8–10GB of free RAM depending on your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Qwen3's thinking mode and how do I activate it?&lt;/strong&gt;&lt;br&gt;
It's the model's ability to generate an explicit reasoning chain before delivering the final response. You activate it by including &lt;code&gt;/think&lt;/code&gt; in the prompt or via system parameters according to the official documentation. Reasoning tokens appear in &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; blocks, and the pipeline needs to handle them if it's going to use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Qwen3 better than Llama 3.1 for TypeScript agents?&lt;/strong&gt;&lt;br&gt;
Depends on the use case. For complex reasoning and code, the published comparisons are favorable. For pipelines that already work with structured outputs and don't need reasoning traceability, the switch isn't automatically positive. Evaluate with your own baseline before migrating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Qwen3's MoE models viable on consumer hardware?&lt;/strong&gt;&lt;br&gt;
The 30B-A3B activates roughly 3B parameters per inference, which reduces active compute — but the RAM needed to load the full model is still significant. It's not a 3B model in terms of memory consumption. Check the requirements before assuming it's a lightweight option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Qwen3 handle English well beyond benchmarks?&lt;/strong&gt;&lt;br&gt;
The Qwen team claims support for 119 languages in the official blog. In practice, multilingual support in open models varies by domain and task type. Your own baseline is still necessary for any production pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I wait for the community to test Qwen3 or just install it now?&lt;/strong&gt;&lt;br&gt;
If you have a specific use case where thinking mode or code quality are relevant, installing and testing it locally has almost zero cost. If the pipeline already works and the driver for switching is "the new model dropped," wait until you have a more concrete reason.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision that actually matters
&lt;/h2&gt;

&lt;p&gt;Qwen3 is a genuinely interesting model. The activatable thinking mode, the MoE variants, and the documented multilingual support are real improvements — not empty marketing. If you have a pipeline where traceable reasoning matters, it's worth testing.&lt;/p&gt;

&lt;p&gt;But "worth testing" is not the same as "blow up your setup overnight." The question I ask myself every time a new model drops is the same one I learned to ask diagnosing connection outages at the cyber café: &lt;strong&gt;what specific problem does this solve better than what I have today?&lt;/strong&gt; If the answer is specific, the switch makes sense. If the answer is "the benchmarks are better," that's not enough — and if that's genuinely your only answer, that's the moment to stop and go get a baseline instead of a new model.&lt;/p&gt;

&lt;p&gt;For agent pipelines running Llama 3.1 or 3.2 that already work: spin up Qwen3 in parallel, run the same prompt set, compare. Without that, any decision is noise. And noise costs time you could be spending on something else.&lt;/p&gt;

&lt;p&gt;If you want to keep exploring AI pipelines from a technical-criteria standpoint rather than hype, the post on &lt;a href="https://juanchi.dev/en/blog/netron-inspect-ml-models-no-jupyter-no-drama" rel="noopener noreferrer"&gt;how to visualize ML models with Netron&lt;/a&gt; is a good companion: same philosophy, different angle.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-8B" rel="noopener noreferrer"&gt;Qwen3 — Hugging Face Model Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qwenlm.github.io/blog/qwen3/" rel="noopener noreferrer"&gt;Qwen Blog — Alibaba (official announcement)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;Ollama — Model Library: qwen3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/qwen3-ollama-local-architecture-inference-comparison" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>inferencialocal</category>
      <category>agentesia</category>
    </item>
    <item>
      <title>Qwen3 en local con Ollama: qué cambió en la arquitectura y si vale el cambio</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Sun, 02 Aug 2026 12:00:11 +0000</pubDate>
      <link>https://dev.to/jtorchia/qwen3-en-local-con-ollama-que-cambio-en-la-arquitectura-y-si-vale-el-cambio-4285</link>
      <guid>https://dev.to/jtorchia/qwen3-en-local-con-ollama-que-cambio-en-la-arquitectura-y-si-vale-el-cambio-4285</guid>
      <description>&lt;h1&gt;
  
  
  Qwen3 en local con Ollama: qué cambió en la arquitectura y si vale el cambio
&lt;/h1&gt;

&lt;p&gt;En 2005, cuando administraba el cyber a los 16, aprendí algo que todavía aplico: no cambies lo que funciona hasta que puedas demostrar que lo nuevo lo supera en tu escenario. No en el benchmark de otro. No en el anuncio del fabricante. En el tuyo. Cada vez que actualizábamos algo sin criterio, alguien terminaba diagnosticando un corte de conexión a las 11pm con el local lleno de pibes esperando para jugar.&lt;/p&gt;

&lt;p&gt;Hoy veo lo mismo cuando sale un modelo nuevo. Sale Qwen3, Twitter explota, y la pregunta que nadie se hace es la única que importa: &lt;strong&gt;¿vale la pena reemplazar el modelo que ya tenés en Ollama, o es más hype que sustancia?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mi tesis es esta: Qwen3 es genuinamente interesante para inferencia local, pero no por las razones que circulan en los hilos virales. El thinking mode activable por prompt no es "más inteligencia", es un control explícito sobre cuándo pagás el costo de tokens extra a cambio de trazabilidad. Esa es la diferencia real con Llama 3.1/3.2, y es la que determina si migrar tiene sentido para tu pipeline específico, no un promedio de benchmarks que armó el propio equipo de Alibaba.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qwen3 en Ollama: qué dice la arquitectura (y qué no dice)
&lt;/h2&gt;

&lt;p&gt;Qwen3 está disponible en Ollama (&lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;ollama.com/library/qwen3&lt;/a&gt;) en múltiples tamaños: 0.6B, 1.7B, 4B, 8B, 14B, 30B-A3B (MoE), 32B y 235B-A22B (MoE). Eso ya es una señal: el equipo de Qwen no apunta solo al extremo de performance, sino al rango de modelos que un desarrollador puede correr en hardware razonable.&lt;/p&gt;

&lt;p&gt;Lo que el &lt;a href="https://qwenlm.github.io/blog/qwen3/" rel="noopener noreferrer"&gt;blog oficial de Qwen&lt;/a&gt; y la &lt;a href="https://huggingface.co/Qwen/Qwen3-8B" rel="noopener noreferrer"&gt;model card en Hugging Face&lt;/a&gt; documentan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thinking mode activable por prompt&lt;/strong&gt;: Qwen3 soporta razonamiento explícito (cadena de pensamiento) que se puede activar o desactivar según el caso de uso. Sirve en pipelines donde necesitás trazabilidad del razonamiento, sin mantener dos modelos distintos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variantes MoE&lt;/strong&gt; (Mixture of Experts): los modelos 30B-A3B y 235B-A22B activan solo una fracción de parámetros por inferencia. En papel, eso reduce el costo computacional para el tamaño total del modelo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soporte multilingüe extendido&lt;/strong&gt;: el equipo declara soporte para 119 idiomas, incluyendo español. Relevante para agentes hispanohablantes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mejoras en razonamiento y código&lt;/strong&gt;: las comparativas publicadas por el equipo de Qwen muestran resultados sólidos en benchmarks de código y matemáticas frente a modelos de generaciones anteriores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Qué no dice esa evidencia&lt;/strong&gt;: los benchmarks publicados son los que el propio equipo seleccionó. No tengo logs propios de producción con Qwen3, y no los voy a inventar. Lo que sí puedo hacer es darte un criterio técnico reproducible para que decidas con tu propia medición.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente al adoptar un modelo nuevo
&lt;/h2&gt;

&lt;p&gt;El error más común no es técnico: es de criterio. El patrón que veo repetirse en foros y en charlas de pasillo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sale un modelo nuevo con benchmarks llamativos.&lt;/li&gt;
&lt;li&gt;Alguien lo prueba con un prompt suelto y "anda bien".&lt;/li&gt;
&lt;li&gt;Lo meten en el pipeline sin baseline claro.&lt;/li&gt;
&lt;li&gt;En algún punto algo empieza a fallar en producción de forma rara, y nadie puede afirmar con certeza si fue el cambio de modelo o el contexto que se armó distinto. Es una hipótesis de manual, no un log que tenga yo: si migrás sin comparar antes/después con los mismos prompts, perdés la capacidad de diagnosticarlo cuando pase.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para un pipeline de agentes con TypeScript y Ollama, el costo oculto de cambiar de modelo es más alto de lo que parece:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cambios en el formato de output&lt;/strong&gt;: Qwen3 puede generar tokens de &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; cuando el thinking mode está activo. Si el parser del agente no espera ese bloque, va a romper el JSON o el texto que consume el downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context window diferente&lt;/strong&gt;: Qwen3-8B declara una context window de 128K tokens según la model card. Si el pipeline asume un límite menor, puede comportarse distinto de formas sutiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperatura y sampling&lt;/strong&gt;: cada modelo tiene un espacio de sampling diferente. Lo que funcionaba con Llama 3.1 con &lt;code&gt;temperature: 0.7&lt;/code&gt; no se transfiere directamente.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Un punto de control básico antes de migrar modelos en un pipeline Ollama&lt;/span&gt;
&lt;span class="c1"&gt;// No es una garantía, es un checklist de fricción mínima&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qwen3:8b&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Desactivar thinking mode si no necesitás trazabilidad explícita&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;num_ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Arrancá conservador, no asumas que 128K es gratis en RAM&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// Si el pipeline parsea JSON estructurado, añadí validación de bloques &amp;lt;think&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Antes de deployar: corré el mismo conjunto de prompts con el modelo anterior&lt;/span&gt;
&lt;span class="c1"&gt;// y con Qwen3, y comparás outputs. Sin baseline, no hay decisión.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El thinking mode de Qwen3 es real y útil, pero requiere que el pipeline lo maneje explícitamente. Si no lo hacés, estás pagando el costo de tokens extra sin capturar el beneficio. Eso es lo incómodo que nadie menciona en los hilos de lanzamiento: la ventaja no es gratis, hay que codearla.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matriz de decisión: cuándo tiene sentido cambiar a Qwen3
&lt;/h2&gt;

&lt;p&gt;Esta es la herramienta que me resulta más útil cuando evalúo un cambio de modelo. No es evidencia de producción propia, es criterio técnico prudente basado en lo que la documentación pública permite afirmar.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;¿Qwen3 suma?&lt;/th&gt;
&lt;th&gt;Razón&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agente que necesita razonamiento trazable&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sí, probá&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Thinking mode activable por prompt es una ventaja real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline de generación de código TypeScript/Python&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sí, probá&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Las mejoras en código están documentadas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agente que parsea JSON estricto sin capa de validación&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No todavía&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Los tokens &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; pueden romper el parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline hispanohablante con Llama 3.1 que ya funciona&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Evaluá primero&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;El salto no es garantizado sin baseline propio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware con menos de 16GB RAM y modelo 8B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Con cuidado&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;128K context window tiene costo de memoria real&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caso de uso con modelo MoE (30B-A3B) en hardware limitado&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Probá en local antes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MoE reduce cómputo activo, pero RAM total sigue alta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La lógica detrás de cada fila: si el thinking mode es relevante para el caso de uso, Qwen3 tiene una ventaja concreta. Si el pipeline ya funciona y no necesitás esa capacidad, el riesgo de migración supera el beneficio esperado sin datos propios.&lt;/p&gt;

&lt;p&gt;Esto conecta con algo que ya planteé en el post sobre &lt;a href="https://juanchi.dev/es/blog/nodejs-runtime-javascript-backend-event-loop-ecosystem" rel="noopener noreferrer"&gt;Node.js y el event loop&lt;/a&gt;: los cambios de runtime o modelo se evalúan en contexto, no en abstracto. La abstracción es cómoda para escribir un thread, pero no paga las cuentas cuando el agente falla a las 3am.&lt;/p&gt;




&lt;h2&gt;
  
  
  Límites honestos: qué no podés concluir con esta evidencia
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar, necesito ser explícito sobre lo que esta evidencia no permite afirmar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No sé si Qwen3 es "mejor" que Llama 3.1/3.2 en tu pipeline&lt;/strong&gt;: eso depende del caso de uso, los prompts, el hardware y cómo está estructurado el agente. Los benchmarks publicados son orientativos, no decisivos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sé el consumo de RAM real en tu setup&lt;/strong&gt;: el modelo 8B con context window grande puede exceder lo que sugiere el spec técnico dependiendo del backend de Ollama y el sistema operativo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No sé si el thinking mode va a ayudar o molestar&lt;/strong&gt;: en pipelines que esperan outputs cortos y estructurados, los tokens de razonamiento pueden ser ruido costoso. En pipelines donde la calidad del razonamiento importa más que la latencia, pueden valer la pena.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los benchmarks de Alibaba los eligió Alibaba&lt;/strong&gt;: eso no los invalida, pero es un dato para pesar la evidencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si querés validar, el camino reproducible es: levantás Qwen3 en local con Ollama, corrés el mismo conjunto de prompts de tu pipeline con el modelo anterior y con Qwen3, y comparás. Sin eso, cualquier conclusión es especulación con formato de post técnico.&lt;/p&gt;

&lt;p&gt;Esto también aplica a decisiones de infraestructura más amplias, como cuando discutí &lt;a href="https://juanchi.dev/es/blog/spring-boot-actuator-endpoints-seguridad-3" rel="noopener noreferrer"&gt;qué exponer y qué ocultar en Spring Boot Actuator&lt;/a&gt;: el principio es el mismo, no cambies lo que no mediste.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: Qwen3, Ollama e inferencia local
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo instalo Qwen3 en Ollama?&lt;/strong&gt;&lt;br&gt;
Con un comando: &lt;code&gt;ollama pull qwen3:8b&lt;/code&gt;. Reemplazá &lt;code&gt;8b&lt;/code&gt; con el tamaño que corresponda a tu hardware. Los tamaños disponibles están en &lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;ollama.com/library/qwen3&lt;/a&gt;. Para el 8B necesitás al menos 8-10GB de RAM libre dependiendo del sistema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es el thinking mode de Qwen3 y cómo lo activo?&lt;/strong&gt;&lt;br&gt;
Es la capacidad del modelo de generar una cadena de razonamiento explícita antes de dar la respuesta final. Se activa incluyendo &lt;code&gt;/think&lt;/code&gt; en el prompt o mediante parámetros del sistema según la documentación oficial. Los tokens de razonamiento aparecen en bloques &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; y el pipeline los tiene que manejar si los espera.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qwen3 es mejor que Llama 3.1 para agentes en TypeScript?&lt;/strong&gt;&lt;br&gt;
Depende del caso de uso. Para razonamiento complejo y código, las comparativas publicadas son favorables. Para pipelines que ya funcionan con outputs estructurados y no necesitan trazabilidad de razonamiento, el salto no es automáticamente positivo. Evaluá con baseline propio antes de migrar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Los modelos MoE de Qwen3 son viables en hardware de consumo?&lt;/strong&gt;&lt;br&gt;
El 30B-A3B activa aproximadamente 3B parámetros por inferencia, lo que reduce el cómputo activo, pero la RAM necesaria para cargar el modelo completo sigue siendo significativa. No es un modelo de 3B en consumo de memoria. Revisá los requerimientos antes de asumirlo como opción liviana.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qwen3 soporta español bien?&lt;/strong&gt;&lt;br&gt;
El equipo de Qwen declara soporte para 119 idiomas incluyendo español en el blog oficial. En la práctica, el soporte multilingüe en modelos abiertos varía según el dominio y el tipo de tarea. Para pipelines hispanohablantes, el baseline propio sigue siendo necesario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Conviene esperar a que la comunidad pruebe Qwen3 o lo instalo ya?&lt;/strong&gt;&lt;br&gt;
Si tenés un caso de uso específico donde el thinking mode o la calidad en código son relevantes, instalarlo y probarlo localmente tiene costo casi nulo. Si el pipeline ya funciona y el driver del cambio es "el modelo nuevo salió", esperá a tener un criterio más concreto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cierre: la decisión que importa
&lt;/h2&gt;

&lt;p&gt;Qwen3 es un modelo genuinamente interesante. El thinking mode activable, las variantes MoE y el soporte multilingüe documentado son mejoras reales, no marketing vacío. Si tenés un pipeline donde el razonamiento trazable importa, vale la pena probarlo.&lt;/p&gt;

&lt;p&gt;Pero "vale la pena probarlo" no es lo mismo que "cambiá el setup de golpe". La pregunta que me hago cada vez que sale un modelo nuevo es la misma que aprendí a hacerme diagnosticando cortes de conexión en el cyber: &lt;strong&gt;¿qué problema concreto resuelve esto mejor que lo que tengo hoy?&lt;/strong&gt; Si la respuesta es específica, el cambio tiene sentido. Si la respuesta es "los benchmarks son mejores", eso no alcanza, y lo digo habiendo migrado sistemas por peores razones que esa.&lt;/p&gt;

&lt;p&gt;Para los pipelines de agentes hispanohablantes con Llama 3.1 o 3.2 que ya funcionan: levantá Qwen3 en paralelo, corré el mismo conjunto de prompts, comparás. Sin eso, cualquier decisión es ruido. Y el ruido cuesta tiempo que podrías estar invirtiendo en otra cosa. La pregunta incómoda que dejo sobre la mesa: ¿estás migrando porque el modelo resuelve algo que el actual no resuelve, o porque te da vergüenza seguir usando "el viejo"?&lt;/p&gt;

&lt;p&gt;Si querés seguir explorando pipelines de IA desde criterio técnico y no desde hype, el post sobre &lt;a href="https://juanchi.dev/es/blog/netron-visualizador-modelos-ml-onnx-tensorflow-pytorch" rel="noopener noreferrer"&gt;cómo visualizar modelos ML con Netron&lt;/a&gt; es un buen complemento: misma filosofía, otro ángulo.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuentes originales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/Qwen/Qwen3-8B" rel="noopener noreferrer"&gt;Qwen3 — Hugging Face Model Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://qwenlm.github.io/blog/qwen3/" rel="noopener noreferrer"&gt;Qwen Blog — Alibaba (anuncio oficial)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library/qwen3" rel="noopener noreferrer"&gt;Ollama — Model Library: qwen3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/qwen3-ollama-local-inferencia-comparativa" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>inferencialocal</category>
    </item>
    <item>
      <title>Prisma Query Logging and PostgreSQL: Where the ORM Ends and the Database Begins</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:00:16 +0000</pubDate>
      <link>https://dev.to/jtorchia/prisma-query-logging-and-postgresql-where-the-orm-ends-and-the-database-begins-1e36</link>
      <guid>https://dev.to/jtorchia/prisma-query-logging-and-postgresql-where-the-orm-ends-and-the-database-begins-1e36</guid>
      <description>&lt;p&gt;You turned on &lt;code&gt;log: ['query']&lt;/code&gt; on the Prisma client, watched the console fill up with SELECTs and their parameters, and that's where the investigation stopped. The endpoint is still slow. The log tells you what SQL ran, but it doesn't tell you if that SQL used an index, if it waited on a lock, or if the problem is the query itself or the connection running it. That gap between "I can see the query" and "I understand why it's slow" is what this post is about.&lt;/p&gt;

&lt;p&gt;My thesis: &lt;strong&gt;the log and the database answer two different categories of question, and the mistake isn't using one or the other — it's not knowing which question you're actually asking before you open either.&lt;/strong&gt; Almost every "slow endpoint" investigation I've seen (my own included, more than once) starts by staring at the log as if staring harder would eventually reveal a lock or a missing index. It won't. The log was never built to show you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prisma query logging PostgreSQL: what the ORM's log actually solves
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging" rel="noopener noreferrer"&gt;official Prisma docs on logging&lt;/a&gt; are clear about the scope: the logging system lets you subscribe to &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt; events from the client, and every query-type event includes the generated SQL, the parameters, and the total duration of that call. That's the whole promise. Nothing about execution plans, nothing about locks, nothing about what happens inside PostgreSQL once it receives that query.&lt;/p&gt;

&lt;p&gt;Setting it up looks like this:&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="c1"&gt;// prisma-client.ts — basic logging with levels&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;log&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="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evento&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="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SQL:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Parametros:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Duracion (ms):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&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;With this you already get everything the docs promise: exact SQL, parameters, total round-trip duration. That's enough to answer ORM-type questions: Is Prisma generating a JOIN I wasn't expecting? Is this &lt;code&gt;findMany&lt;/code&gt; with a nested &lt;code&gt;include&lt;/code&gt; firing 40 queries instead of one? Is some middleware running something twice? The log alone answers those questions, no need to touch the database.&lt;/p&gt;

&lt;p&gt;What it does NOT answer — and to be fair to the source, the docs never claim it does — is why a specific query takes 800ms instead of 8ms. That lives on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where people get it wrong: using the log as if it were a database profiler
&lt;/h2&gt;

&lt;p&gt;The common recipe: turn on the log, notice a query taking forever, copy it, run it by hand in the SQL client, it runs "fast" there because the table's cached in the plan or because data volume at that moment is different, and close the ticket saying "it fixed itself." Three months later it happens again.&lt;/p&gt;

&lt;p&gt;The hidden cost is that the duration number Prisma reports includes the full round trip: parameter serialization, network time to PostgreSQL, execution time in the database, and deserialization of the result back in the TypeScript client. If the connection pool is saturated or there's network latency, that number goes up without the query itself having any problem at all. The log gives you a total, not a breakdown.&lt;/p&gt;

&lt;p&gt;Classic counterexample: a query the log shows as 300ms could be a 2ms query in PostgreSQL that spent 298ms waiting to grab a connection from the pool. If the diagnosis stops at "this query is slow, gotta optimize it," the real problem — pool configuration, number of concurrent connections, a badly set timeout — never gets touched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Request llega] --&amp;gt; B[Prisma pide conexion al pool]
  B --&amp;gt; C{Pool disponible?}
  C --&amp;gt;|no, espera| D[Tiempo de espera se suma al log]
  C --&amp;gt;|si| E[PostgreSQL ejecuta la query]
  E --&amp;gt; F[Prisma deserializa resultado]
  D --&amp;gt; E
  F --&amp;gt; G[Log reporta duracion total]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diagram shows why the log's number mixes together things worth separating before you touch any code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision checklist: when the log is enough and when to look at PostgreSQL
&lt;/h2&gt;

&lt;p&gt;This is the cutoff criteria I use before sinking time into either side:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Prisma log is enough when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The suspicion is about what SQL the ORM generates (N+1 queries, includes that blow up, unnecessary selects).&lt;/li&gt;
&lt;li&gt;The problem shows up always, regardless of data volume, in any environment.&lt;/li&gt;
&lt;li&gt;You can reproduce it with an isolated call and see the exact SQL in the console.&lt;/li&gt;
&lt;li&gt;The question is "is this doing what I think it's doing?" and not "is this fast?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You need to look at PostgreSQL directly when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A specific query takes different amounts of time depending on the time of day or data volume.&lt;/li&gt;
&lt;li&gt;The log shows high durations but the SQL, run by hand, "runs fine" — that's when the problem isn't the query.&lt;/li&gt;
&lt;li&gt;You suspect locks, contention, or concurrent queries stepping on each other.&lt;/li&gt;
&lt;li&gt;You need to know if an index is actually being used or not — that's what &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; tells you, not the ORM's log.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL tools that fall into this second category: &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; on the exact query you captured in the log, the &lt;code&gt;pg_stat_statements&lt;/code&gt; extension to see real accumulated execution stats over time, and &lt;code&gt;pg_stat_activity&lt;/code&gt; to see what's running right now and whether anything's blocked. None of these get replaced by Prisma, and that's fine — it's not Prisma's job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to check first, in order:&lt;/strong&gt; Prisma's log to confirm the exact SQL → run it with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; to see the actual plan → if the plan looks reasonable but the log still shows high times, suspect the connection pool before suspecting the query.&lt;/p&gt;

&lt;p&gt;I apply a version of this same split — find which layer actually caused the problem instead of fixing the layer where it just happens to surface — in other parts of the stack too. It's the same instinct behind checking &lt;a href="https://juanchi.dev/en/blog/npm-dependencies-how-to-evaluate-before-production" rel="noopener noreferrer"&gt;how to evaluate a library before shipping it to production&lt;/a&gt; before blaming a dependency for something that's actually a config issue, or behind tracing a TypeScript error back to a badly defined type further up instead of patching the symptom, like I laid out in &lt;a href="https://juanchi.dev/en/blog/strict-null-checks-typescript-production-failures" rel="noopener noreferrer"&gt;strict null checks in production&lt;/a&gt;. Different tools, same discipline: don't fix where it hurts, fix where it broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits: what this evidence doesn't let you conclude
&lt;/h2&gt;

&lt;p&gt;Neither the Prisma docs nor this post give a figure for how much overhead logging itself adds, nor a number like "past X queries per second it's worth instrumenting the database." Any claim like that would need a reproducible experiment with controlled load, and I don't have one — I'd rather not make it up.&lt;/p&gt;

&lt;p&gt;You also can't conclude, just from Prisma's log, whether an index is missing, whether a table needs partitioning, or whether the problem is schema design. Those conclusions require looking at PostgreSQL's real execution plan, not the client's report.&lt;/p&gt;

&lt;p&gt;And an honest note about the source: Prisma's docs don't compare their logging against database observability tools, nor do they suggest it's a substitute. The limit I'm laying out in this post is mine, not something the docs contradict or explicitly back up — it's a reading of what the log promises versus what it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take and the next step
&lt;/h2&gt;

&lt;p&gt;I use Prisma's log for everything that's about SQL shape: confirming the ORM generated what I expected, catching N+1s before they hit an environment with real data, checking that an &lt;code&gt;include&lt;/code&gt; isn't pulling in extra relations. For that it's fast and needs nothing else.&lt;/p&gt;

&lt;p&gt;The moment the question shifts from "what SQL is this?" to "why is it slow?", I close the log console and open &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. Mixing both questions into the same tool is exactly what keeps people staring at logs without moving forward.&lt;/p&gt;

&lt;p&gt;The uncomfortable part, if I'm honest: most teams don't skip &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; because it's hard, they skip it because the Prisma log already feels like "doing observability," and it's not — it's doing a third of it. If you're bringing more serious observability into your stack — structured logs, pool metrics, tracing — ask first what layer each tool actually covers before adding it, the same question I asked when I brought an external model into the code pipeline in &lt;a href="https://juanchi.dev/en/blog/deepseek-api-typescript-secure-integration-model-evaluation" rel="noopener noreferrer"&gt;DeepSeek API in TypeScript&lt;/a&gt;. Adding a tool because it feels thorough is how you end up with five dashboards and still no answer for why the endpoint is slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Prisma's log show the query's execution plan?&lt;/strong&gt;&lt;br&gt;
No. It shows the generated SQL, the parameters, and the total round-trip duration. The execution plan you have to request separately with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; directly in PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does turning on &lt;code&gt;log: ['query']&lt;/code&gt; in production have a cost?&lt;/strong&gt;&lt;br&gt;
It adds serialization and logging overhead per query, especially if the emitted level is &lt;code&gt;stdout&lt;/code&gt; instead of &lt;code&gt;event&lt;/code&gt; with a lightweight handler. The official docs don't give an exact figure for that cost, so it's worth measuring it in your own environment before assuming it's negligible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Prisma's log to detect N+1 queries?&lt;/strong&gt;&lt;br&gt;
Yes, it's one of the most direct uses: if a &lt;code&gt;findMany&lt;/code&gt; with relations fires dozens of individual queries in the log, that's your N+1 right there. It's exactly the kind of question the log answers well because it's about SQL shape, not database performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;pg_stat_statements&lt;/code&gt; replace Prisma's logging?&lt;/strong&gt;&lt;br&gt;
It doesn't replace it, it complements it. &lt;code&gt;pg_stat_statements&lt;/code&gt; accumulates real execution stats inside PostgreSQL over time; Prisma's log gives you the point-in-time view of each call from the client. They answer different questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does a query the log flags as slow run fast when I execute it by hand?&lt;/strong&gt;&lt;br&gt;
Because the log's number includes network time and connection pool wait time, not just execution in the database. If you run the query in isolation in a SQL client, you skip that wait and see only PostgreSQL's real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Prisma's logging useful for diagnosing locks or contention?&lt;/strong&gt;&lt;br&gt;
Not directly. For that you need to look at &lt;code&gt;pg_stat_activity&lt;/code&gt; in PostgreSQL, which shows which sessions are running and whether any of them are blocked waiting on another. Prisma's log has no visibility into that.&lt;/p&gt;




&lt;p&gt;Original source: &lt;a href="https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging" rel="noopener noreferrer"&gt;Prisma logging docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/prisma-query-logging-postgresql-orm-limits" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>observabilidad</category>
    </item>
    <item>
      <title>Prisma Query Logging y PostgreSQL: dónde termina el ORM y empieza la base</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 29 Jul 2026 12:00:11 +0000</pubDate>
      <link>https://dev.to/jtorchia/prisma-query-logging-y-postgresql-donde-termina-el-orm-y-empieza-la-base-5bo</link>
      <guid>https://dev.to/jtorchia/prisma-query-logging-y-postgresql-donde-termina-el-orm-y-empieza-la-base-5bo</guid>
      <description>&lt;p&gt;Activaste &lt;code&gt;log: ['query']&lt;/code&gt; en el cliente de Prisma, viste la consola llenarse de SELECTs con sus parámetros, y ahí quedó la investigación. El endpoint sigue lento. El log te dice qué SQL se ejecutó, pero no te dice si ese SQL usó un índice, si esperó un lock, o si el problema es la query en sí o la conexión que la ejecuta. Esa distancia entre "veo la query" y "entiendo por qué tarda" es el tema de este post.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple y no es nueva, pero acá casi nadie la aplica con disciplina: &lt;strong&gt;el query logging de Prisma sirve para encontrar patrones — queries N+1, SQL inesperado, parámetros raros — pero no reemplaza instrumentar PostgreSQL cuando el problema es de rendimiento real.&lt;/strong&gt; Son dos capas distintas, resuelven preguntas distintas, y mezclarlas es la razón por la que tanta gente mira logs durante media hora sin llegar a ninguna conclusión.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prisma query logging PostgreSQL: qué resuelve el log del ORM
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging" rel="noopener noreferrer"&gt;documentación oficial de Prisma sobre logging&lt;/a&gt; es clara sobre el alcance: el sistema de logs te permite suscribirte a eventos &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt; y &lt;code&gt;error&lt;/code&gt; del cliente, y cada evento de tipo query incluye el SQL generado, los parámetros y la duración total de esa llamada. Eso es todo lo que promete. No dice nada de planes de ejecución, no dice nada de locks, no dice nada de qué pasa dentro de PostgreSQL cuando recibe esa query.&lt;/p&gt;

&lt;p&gt;Configurarlo es así:&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="c1"&gt;// prisma-client.ts — logging basico con niveles&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;log&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="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evento&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="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SQL:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Parametros:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Duracion (ms):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evento&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&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;Con esto ya tenés lo que la doc promete: SQL exacto, parámetros, duración total del round trip. Es suficiente para responder preguntas de tipo ORM: ¿Prisma está generando un JOIN que no esperabas? ¿Este &lt;code&gt;findMany&lt;/code&gt; con &lt;code&gt;include&lt;/code&gt; anidado dispara 40 queries en vez de una? ¿Un middleware está ejecutando algo dos veces? Ese tipo de preguntas las resuelve el log solo, sin tocar la base.&lt;/p&gt;

&lt;p&gt;Lo que NO resuelve — y la doc tampoco lo promete, hay que ser justo con la fuente — es por qué una query puntual tarda 800ms en vez de 8ms. Eso vive del otro lado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente: usar el log como si fuera un profiler de base
&lt;/h2&gt;

&lt;p&gt;La receta común es: activo el log, veo que una query tarda mucho, la copio, la corro a mano en el cliente SQL, ahí anda "rápido" porque la tabla está cacheada en el plan o porque el volumen de datos en ese momento es distinto, y cierro el ticket diciendo "se resolvió solo". Tres meses después vuelve a pasar. Lo vi pasar más de una vez en tickets que se reabren solos: nadie mintió, nadie fue negligente, simplemente se confundió la capa.&lt;/p&gt;

&lt;p&gt;El costo oculto es que el número de duración que reporta Prisma incluye el viaje completo: serialización de parámetros, tiempo de red hacia PostgreSQL, tiempo de ejecución en la base, y deserialización del resultado en el cliente TypeScript. Si el pool de conexiones está saturado o hay latencia de red, ese número sube sin que la query en sí tenga ningún problema. El log te da un total, no un desglose.&lt;/p&gt;

&lt;p&gt;Contraejemplo típico: una query que en el log muestra 300ms de duración puede ser una query de 2ms en PostgreSQL que esperó 298ms para conseguir una conexión del pool. Si el diagnóstico se queda en "esta query es lenta, hay que optimizarla", el problema real — configuración de pool, número de conexiones concurrentes, timeout mal seteado — sigue sin tocarse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Request llega] --&amp;gt; B[Prisma pide conexion al pool]
  B --&amp;gt; C{Pool disponible?}
  C --&amp;gt;|no, espera| D[Tiempo de espera se suma al log]
  C --&amp;gt;|si| E[PostgreSQL ejecuta la query]
  E --&amp;gt; F[Prisma deserializa resultado]
  D --&amp;gt; E
  F --&amp;gt; G[Log reporta duracion total]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El diagrama muestra por qué el número del log mezcla cosas que conviene separar antes de tocar código.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de decisión: cuándo alcanza el log y cuándo mirar PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Esto es lo que uso como criterio de corte antes de invertir tiempo en cualquiera de los dos lados:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alcanza con el log de Prisma cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La sospecha es sobre qué SQL genera el ORM (queries N+1, includes que explotan, selects innecesarios).&lt;/li&gt;
&lt;li&gt;El problema aparece siempre, con cualquier volumen de datos, en cualquier ambiente.&lt;/li&gt;
&lt;li&gt;Podés reproducirlo con una llamada aislada y ver el SQL exacto en la consola.&lt;/li&gt;
&lt;li&gt;La pregunta es "¿esto hace lo que yo creo que hace?" y no "¿esto es rápido?".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hay que mirar PostgreSQL directamente cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una query específica tarda distinto según el momento del día o el volumen de datos.&lt;/li&gt;
&lt;li&gt;El log muestra duraciones altas pero el SQL, corrido a mano, "anda bien" — ahí el problema no es la query.&lt;/li&gt;
&lt;li&gt;Sospechás de locks, contención, o queries concurrentes pisándose.&lt;/li&gt;
&lt;li&gt;Necesitás saber si un índice se está usando o no — eso lo dice &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, no el log del ORM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Herramientas de PostgreSQL que entran en esta segunda categoría: &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt; sobre la query exacta que capturaste en el log, la extensión &lt;code&gt;pg_stat_statements&lt;/code&gt; para ver acumulados reales de ejecución en el tiempo, y &lt;code&gt;pg_stat_activity&lt;/code&gt; para ver qué está corriendo ahora mismo y si hay algo bloqueado. Ninguna de estas la reemplaza Prisma, y está bien que sea así — no es su trabajo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qué mirar primero, en orden:&lt;/strong&gt; log de Prisma para confirmar el SQL exacto → correrlo con &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; para ver el plan real → si el plan es razonable pero el log sigue mostrando tiempos altos, sospechar del pool de conexiones antes que de la query.&lt;/p&gt;

&lt;p&gt;Lo incómodo de este criterio es que obliga a soltar la primera hipótesis. Cuesta más aceptar "no es la query, es el pool" que quedarte reescribiendo el SELECT una vez más, porque tocar la query da la sensación de estar avanzando aunque no cambie nada. Este mismo criterio de "separar la capa que generó el problema de la capa donde se manifiesta" es el mismo tipo de pregunta que me hago cuando evalúo si vale la pena sumar una dependencia nueva al proyecto — ver &lt;a href="https://juanchi.dev/es/blog/evaluar-dependencias-npm-seguridad-mantenimiento-2" rel="noopener noreferrer"&gt;cómo evaluar una librería antes de meterla en producción&lt;/a&gt; — o cuando el compilador de TypeScript marca un error que en realidad viene de un tipo mal definido más arriba, como escribí en &lt;a href="https://juanchi.dev/es/blog/strict-null-checks-typescript-produccion" rel="noopener noreferrer"&gt;strict null checks en producción&lt;/a&gt;. No es la misma herramienta ni el mismo bug, pero la pregunta de fondo —¿dónde nació esto realmente?— se repite en cualquier capa del stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites: lo que esta evidencia no permite concluir
&lt;/h2&gt;

&lt;p&gt;Ni la documentación de Prisma ni este post dan una cifra de cuánto overhead agrega el logging en sí, ni un número de "a partir de tantas queries por segundo conviene instrumentar la base". Cualquier claim de ese tipo necesitaría un experimento reproducible con carga controlada, y no lo tengo — y prefiero no inventarlo.&lt;/p&gt;

&lt;p&gt;Tampoco se puede concluir, solo con el log de Prisma, si un índice falta, si una tabla necesita particionado, o si el problema es de diseño de esquema. Esas conclusiones requieren mirar el plan de ejecución real de PostgreSQL, no el reporte del cliente.&lt;/p&gt;

&lt;p&gt;Y una aclaración honesta sobre la fuente: la doc de Prisma no compara su logging contra herramientas de observabilidad de base de datos ni sugiere que sea un sustituto. El límite que planteo en este post es mío, no algo que la doc contradiga ni respalde explícitamente — es una lectura de lo que el log promete versus lo que no promete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi postura y el próximo paso
&lt;/h2&gt;

&lt;p&gt;Uso el log de Prisma para todo lo que es forma del SQL: confirmar que el ORM generó lo que esperaba, cazar N+1 antes de que lleguen a un ambiente con datos reales, revisar que un &lt;code&gt;include&lt;/code&gt; no esté trayendo relaciones de más. Para eso es rápido y no necesita nada extra.&lt;/p&gt;

&lt;p&gt;En el momento en que la pregunta cambia de "¿qué SQL es?" a "¿por qué tarda?", cierro la consola del log y abro &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. Mezclar las dos preguntas en la misma herramienta es lo que hace que la gente se quede mirando logs sin avanzar, y es la trampa más fácil de caer porque las dos preguntas usan el mismo texto de SQL en pantalla.&lt;/p&gt;

&lt;p&gt;Si estás evaluando meter observabilidad más seria al stack — logs estructurados, métricas de pool, tracing — vale la misma lógica que aplico cuando incorporo cualquier pieza nueva al backend: primero entender qué capa resuelve, después decidir si hace falta. Ese mismo filtro lo usé evaluando un modelo externo en el pipeline de código — &lt;a href="https://juanchi.dev/es/blog/deepseek-api-typescript-integracion-segura" rel="noopener noreferrer"&gt;DeepSeek API en TypeScript&lt;/a&gt; — antes de sumarlo: separar lo que la herramienta promete de lo que uno quiere que prometa evita bastante frustración después.&lt;/p&gt;

&lt;p&gt;Si tenés que elegir una sola cosa para hoy: la próxima vez que el log te muestre una query "lenta", antes de tocar el SQL, corré esa misma query con &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; aislada. Si el plan sale limpio y rápido, el problema no está en la query — está en algo que el log nunca te iba a mostrar.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿El log de Prisma muestra el plan de ejecución de la query?&lt;/strong&gt;&lt;br&gt;
No. Muestra el SQL generado, los parámetros y la duración total del round trip. El plan de ejecución hay que pedirlo aparte con &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; directamente en PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Activar &lt;code&gt;log: ['query']&lt;/code&gt; en producción tiene costo?&lt;/strong&gt;&lt;br&gt;
Agrega overhead de serialización y logging por cada query, sobre todo si el nivel emitido es &lt;code&gt;stdout&lt;/code&gt; en vez de &lt;code&gt;event&lt;/code&gt; con un handler liviano. La doc oficial no da una cifra exacta de ese costo, así que conviene medirlo en el ambiente propio antes de asumir que es despreciable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar el log de Prisma para detectar queries N+1?&lt;/strong&gt;&lt;br&gt;
Sí, es uno de los usos más directos: si un &lt;code&gt;findMany&lt;/code&gt; con relaciones dispara decenas de queries individuales en el log, ahí está el N+1. Es justamente el tipo de pregunta que el log resuelve bien porque es sobre forma de SQL, no sobre rendimiento de la base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;pg_stat_statements&lt;/code&gt; reemplaza al logging de Prisma?&lt;/strong&gt;&lt;br&gt;
No lo reemplaza, lo complementa. &lt;code&gt;pg_stat_statements&lt;/code&gt; acumula estadísticas reales de ejecución dentro de PostgreSQL a lo largo del tiempo; el log de Prisma te da la vista puntual de cada llamada desde el cliente. Sirven para preguntas distintas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué una query que el log marca como lenta corre rápido si la ejecuto a mano?&lt;/strong&gt;&lt;br&gt;
Porque el número del log incluye tiempo de red y espera de conexión del pool, no solo ejecución en la base. Si corrés la query aislada en un cliente SQL, te salteás esa espera y ves solo el tiempo real de PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Sirve el logging de Prisma para diagnosticar locks o contención?&lt;/strong&gt;&lt;br&gt;
No directamente. Para eso hay que mirar &lt;code&gt;pg_stat_activity&lt;/code&gt; en PostgreSQL, que muestra qué sesiones están corriendo y si alguna está bloqueada esperando a otra. El log de Prisma no tiene visibilidad de eso.&lt;/p&gt;




&lt;p&gt;Fuente original: &lt;a href="https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging" rel="noopener noreferrer"&gt;Prisma logging docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/prisma-query-logging-postgresql-limites" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>postgres</category>
    </item>
    <item>
      <title>npm Dependencies: How to Evaluate a Library Before It Hits Production</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:28:09 +0000</pubDate>
      <link>https://dev.to/jtorchia/npm-dependencies-how-to-evaluate-a-library-before-it-hits-production-e60</link>
      <guid>https://dev.to/jtorchia/npm-dependencies-how-to-evaluate-a-library-before-it-hits-production-e60</guid>
      <description>&lt;p&gt;I opened an old &lt;code&gt;package.json&lt;/code&gt; recently — from a practice project, not anything in production — and counted 340 lines in the &lt;code&gt;node_modules&lt;/code&gt; listed by pnpm. One single direct dependency. Everything else, transitive. I didn't pick any of it. Somebody, at some point, needed to solve one specific problem and reached for a library, and that single decision dragged in 340 more packages sitting in that tree. I can't tell you how many of those 340 get audited or read line by line by anyone — probably close to none — but I can tell you that whoever maintains them eventually has to patch a CVE under pressure, because that's just how the ecosystem behaves.&lt;/p&gt;

&lt;p&gt;That's the real problem: &lt;code&gt;npm install&lt;/code&gt; takes three seconds and the decision it represents lasts years.&lt;/p&gt;

&lt;p&gt;My thesis is simple and not original, but almost nobody applies it with discipline: &lt;strong&gt;adding a dependency also means taking on maintenance&lt;/strong&gt;. It's not "using someone else's code for free." It's signing a tacit contract where you become responsible for that code continuing to work, staying secure, and staying compatible with whatever the project needs two years from now — even if the original author abandoned the repo eighteen months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluating npm Dependencies: Security and Maintenance Are Not the Same Thing
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;official npm documentation on packages and modules&lt;/a&gt; explains well what a package is, how &lt;code&gt;package.json&lt;/code&gt; gets resolved, how the dependency tree works, and semver. It's the right reference for understanding the mechanics.&lt;/p&gt;

&lt;p&gt;What that documentation doesn't say — because it's not its job to say it — is how to evaluate whether a library is a good idea for your project. There's no "maintenance" section in the npm docs, no trust score. The mechanics of installing a package are trivial. The criteria for deciding whether it's worth installing is a completely different problem, and that's where most people improvise.&lt;/p&gt;

&lt;p&gt;Security and maintenance aren't synonyms either, even though they get treated as if they were. A library can have zero known vulnerabilities today and be completely abandoned — nobody reviewing issues, no releases in two years, a maintainer who stopped responding. That doesn't show up in &lt;code&gt;npm audit&lt;/code&gt;. It shows up when you need someone to fix something and there's nobody on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where People Get It Wrong: The Common Recipe and Its Hidden Cost
&lt;/h2&gt;

&lt;p&gt;The typical recipe when a problem shows up is: search npm, filter by weekly downloads, pick the one with the most GitHub stars, install, move on. It's reasonable as a first filter — downloads and stars correlate with adoption — but as the only criteria it leaves out everything that matters after day one.&lt;/p&gt;

&lt;p&gt;Typical counterexample: a utility library with 2 million weekly downloads, but whose last commit is 3 years old and whose maintainer answered the last issue 14 months ago. High download counts often reflect past adoption, not present health — legacy projects keep pulling it because it's already in the lockfile of thousands of repos, not because anyone's choosing it again today.&lt;/p&gt;

&lt;p&gt;The hidden cost shows up late: Node bumps a major version, something in the runtime changes, the library doesn't get updated because there's nobody to update it, and the team ends up with two ugly options — fork it and maintain it themselves, or migrate all the code depending on it under pressure, with no planning. Neither is free, and both were avoidable if someone had looked at the last commit date before installing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision Matrix: What to Check Before Installing
&lt;/h2&gt;

&lt;p&gt;This is what I go through, in order, before adding a new dependency to a TypeScript project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Active maintenance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Last commit: less than 6 months is a good sign, more than 18 months is a red flag.&lt;/li&gt;
&lt;li&gt;Open vs. closed issues: a ratio heavily skewed toward open suggests nobody's triaging.&lt;/li&gt;
&lt;li&gt;Number of maintainers: just one is a single point of failure — if that person burns out, the project dies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Library surface area&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it solve a specific problem or is it a full framework? The smaller the surface, the less it can break and the less there is to audit.&lt;/li&gt;
&lt;li&gt;How many things does it export that you're not actually going to use? Unused surface area is risk without benefit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. TypeScript types&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does it have its own types or does it depend on a separate &lt;code&gt;@types/package&lt;/code&gt;? Separate types drift out of sync with the actual implementation more often than we'd like to admit.&lt;/li&gt;
&lt;li&gt;If you're working with &lt;code&gt;strict: true&lt;/code&gt; and strict null checks enabled, as I discussed in the post on &lt;a href="https://juanchi.dev/en/blog/strict-null-checks-typescript-production-failures" rel="noopener noreferrer"&gt;strict null checks in production&lt;/a&gt;, a library with badly done types is going to generate implicit &lt;code&gt;any&lt;/code&gt;s that the compiler won't be able to catch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Transitive dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;pnpm why &amp;lt;package&amp;gt;&lt;/code&gt; to see what it drags in. A "lightweight" library can bring in fifteen transitive dependencies you never chose.&lt;/li&gt;
&lt;li&gt;The more transitives, the bigger the attack surface, and the more likely a CVE in a third-level package hits you without you knowing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Exit strategy&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If this library gets abandoned in two years, how much does it cost to pull it out? If it's scattered across the entire codebase with no intermediate abstraction layer, the exit cost is high.&lt;/li&gt;
&lt;li&gt;If it's replaceable with your own code in a day, the dependency risk drops a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick, reproducible check for point 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# See the transitive dependency tree of a package&lt;/span&gt;
pnpm why nombre-del-paquete

&lt;span class="c"&gt;# Audit for known vulnerabilities&lt;/span&gt;
pnpm audit

&lt;span class="c"&gt;# See the last publish date on the npm registry&lt;/span&gt;
npm view nombre-del-paquete time.modified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these commands give you a binary answer. They give you data to decide with judgment, which is different from having an automatic rule. That same principle — evaluating with concrete data instead of trusting a tool's general reputation — is the one I apply when reviewing integrations with external models, like I mentioned when &lt;a href="https://juanchi.dev/en/blog/deepseek-api-typescript-secure-integration-model-evaluation" rel="noopener noreferrer"&gt;evaluating the DeepSeek API in TypeScript&lt;/a&gt;: the question is never "is this tool good in general?", it's "is it good for this specific thing, with this level of maintenance?".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Necesito resolver X] --&amp;gt; B{¿Lo resuelvo en menos de un dia con codigo propio?}
  B --&amp;gt;|si| C[Escribo codigo propio]
  B --&amp;gt;|no| D{¿Mantenimiento activo y tipos propios?}
  D --&amp;gt;|no| E[Buscar alternativa o reconsiderar]
  D --&amp;gt;|si| F{¿Transitivas razonables y salida clara?}
  F --&amp;gt;|no| E
  F --&amp;gt;|si| G[Instalar con abstraccion intermedia]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Limits of This Evaluation
&lt;/h2&gt;

&lt;p&gt;This matrix doesn't replace a real experiment. Everything above is upfront reading criteria — looking at metadata, history, types — it's not the same as running the library under load, measuring its behavior in the project's specific runtime, or seeing how the maintainer reacts to a real issue reported by your own team.&lt;/p&gt;

&lt;p&gt;You also can't conclude, from "actively maintained today," that the library is going to keep being actively maintained next year. Commit history is evidence of past behavior, not a guarantee of future behavior. Projects with lone maintainers can change status overnight — new job, burnout, personal decision — and no checklist predicts that with certainty.&lt;/p&gt;

&lt;p&gt;And watch out with &lt;code&gt;npm audit&lt;/code&gt; specifically: it tells you which vulnerabilities are reported and cataloged today in npm's database. It doesn't tell you which vulnerabilities exist but haven't been reported yet, and it doesn't tell you whether that reported vulnerability actually applies to the usage pattern you have in your project. It's a signal, not a verdict.&lt;/p&gt;

&lt;p&gt;If the project is critical — handles sensitive data, runs in a regulated environment, has an uptime SLA — this matrix is the starting point, not the end. That's where a deeper review is warranted: reading the entire library's source code, not just its README, and considering your own experiment with controlled load before deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do I know if an npm library is well maintained?&lt;/strong&gt;&lt;br&gt;
Look at the date of the last commit and the last release, the number of active maintainers, and the ratio of closed versus open issues. None of these data points alone is decisive, but together they paint a fairly clear picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;npm audit&lt;/code&gt; enough to evaluate a dependency's security?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;npm audit&lt;/code&gt; checks for already-reported, cataloged vulnerabilities. It doesn't detect unknown vulnerabilities and doesn't evaluate whether your specific usage pattern actually exposes you to a listed CVE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it worth using a library without its own TypeScript types?&lt;/strong&gt;&lt;br&gt;
Depends on the surface area. If it's something small with well-maintained &lt;code&gt;@types&lt;/code&gt;, it's acceptable. If it's something central to the architecture, well-maintained proper types should be a requirement, not a luxury.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I check a package's transitive dependencies before installing it?&lt;/strong&gt;&lt;br&gt;
With &lt;code&gt;pnpm why &amp;lt;package&amp;gt;&lt;/code&gt; after installing, or by checking the package's &lt;code&gt;package.json&lt;/code&gt; on the npm registry before deciding. Looking at the tree size with tools like &lt;code&gt;npm ls&lt;/code&gt; in dry-run mode also helps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I do if the library I need is basically abandoned?&lt;/strong&gt;&lt;br&gt;
Weigh the cost of forking it yourself versus writing your own alternative scoped to the actual problem. If the surface you're using is small, a ten-line piece of your own code is often better than an external dependency with no owner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does pnpm change anything about this evaluation compared to npm?&lt;/strong&gt;&lt;br&gt;
The installation mechanics change — pnpm uses a shared content store and is stricter about access to undeclared dependencies — but the criteria for evaluating maintenance, types, and surface area is the same regardless of which package manager you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Stand
&lt;/h2&gt;

&lt;p&gt;I'm not going to tell you to stop using external libraries. That'd be a ridiculous stance coming from someone who works with Next.js, Docker, and half a dozen packages in every project. But every &lt;code&gt;pnpm add&lt;/code&gt; deserves the same question you'd ask about a hire: who's on the other side, and what happens if they disappear?&lt;/p&gt;

&lt;p&gt;Next time you're about to install something to solve a small problem, try first whether you can solve it with a ten-line function of your own. If you can't, run &lt;code&gt;pnpm why&lt;/code&gt; after installing and look at what you brought in the door without actually deciding to. That habit, more than any checklist, is what separates a project that's maintainable three years from now from one that turns into archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt; &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;npm package documentation&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/npm-dependencies-how-to-evaluate-before-production" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>pnpm</category>
      <category>npm</category>
    </item>
    <item>
      <title>Dependencias npm: cómo evaluar una librería antes de meterla en producción</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:28:05 +0000</pubDate>
      <link>https://dev.to/jtorchia/dependencias-npm-como-evaluar-una-libreria-antes-de-meterla-en-produccion-4ccc</link>
      <guid>https://dev.to/jtorchia/dependencias-npm-como-evaluar-una-libreria-antes-de-meterla-en-produccion-4ccc</guid>
      <description>&lt;p&gt;Abrí un &lt;code&gt;package.json&lt;/code&gt; viejo hace poco — de un proyecto de práctica, no de nada productivo — y conté 340 líneas en el &lt;code&gt;node_modules&lt;/code&gt; listado por pnpm. Una sola dependencia directa. El resto, transitivas. Ninguna la elegí yo.&lt;/p&gt;

&lt;p&gt;Y ahí me quedé pensando: en algún momento alguien eligió una librería para resolver un problema puntual, sin pensar en lo que arrastraba. Es un patrón que se repite en cualquier proyecto con un lockfile de más de un año: capas de decisiones ajenas apiladas una arriba de la otra, la mayoría invisibles hasta que un CVE las obliga a mostrarse.&lt;/p&gt;

&lt;p&gt;Ahí está el problema real: &lt;code&gt;npm install&lt;/code&gt; tarda tres segundos y la decisión que representa dura años.&lt;/p&gt;

&lt;p&gt;Mi tesis es simple y no es original, pero casi nadie la aplica con disciplina: &lt;strong&gt;agregar una dependencia también es asumir mantenimiento&lt;/strong&gt;. No es "usar código de otro gratis". Es firmar un contrato tácito donde vos te hacés responsable de que ese código siga funcionando, siga siendo seguro y siga siendo compatible con lo que el proyecto necesite dentro de dos años — aunque el autor original haya abandonado el repo hace dieciocho meses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluar dependencias npm: seguridad y mantenimiento no son lo mismo
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;documentación oficial de npm sobre paquetes y módulos&lt;/a&gt; explica bien qué es un paquete, cómo se resuelve &lt;code&gt;package.json&lt;/code&gt;, cómo funciona el árbol de dependencias y semver. Es la referencia correcta para entender la mecánica.&lt;/p&gt;

&lt;p&gt;Lo que esa documentación no dice — porque no es su trabajo decirlo — es cómo evaluar si una librería es una buena idea para el proyecto. No hay una sección de "mantenimiento" en la doc de npm, ni un score de confiabilidad. La mecánica de instalar un paquete es trivial. El criterio para decidir si conviene instalarlo es un problema completamente distinto, y ahí es donde la mayoría improvisa.&lt;/p&gt;

&lt;p&gt;Seguridad y mantenimiento tampoco son sinónimos, aunque se los trate como si lo fueran. Una librería puede tener cero vulnerabilidades conocidas hoy y estar completamente abandonada — sin nadie revisando issues, sin releases desde hace dos años, con un mainteiner que dejó de responder. Eso no aparece en &lt;code&gt;npm audit&lt;/code&gt;. Aparece cuando necesitás que alguien arregle algo y no hay nadie del otro lado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde se equivoca la gente: la receta común y su costo oculto
&lt;/h2&gt;

&lt;p&gt;La receta típica cuando aparece un problema es: buscar en npm, filtrar por descargas semanales, elegir la que tiene más estrellas en GitHub, instalar, seguir. Es razonable como primer filtro — descargas y estrellas correlacionan con adopción — pero como único criterio deja afuera todo lo que importa después del día uno.&lt;/p&gt;

&lt;p&gt;Contraejemplo típico: una librería de utilidades con 2 millones de descargas semanales, pero cuyo último commit tiene 3 años y cuyo mainteiner respondió el último issue hace 14 meses. Las descargas altas muchas veces reflejan adopción pasada, no salud presente — proyectos legacy siguen bajándola porque ya está en el lockfile de miles de repos, no porque alguien la vuelva a elegir hoy.&lt;/p&gt;

&lt;p&gt;El costo oculto aparece tarde: Node sube de versión mayor, algo en el runtime cambia, la librería no se actualiza porque no hay quién la actualice, y el equipo termina con dos opciones feas — forkearla y mantenerla ellos mismos, o migrar todo el código que depende de ella bajo presión, sin planificación. Ninguna de las dos es gratis, y las dos eran evitables si alguien hubiera mirado la fecha del último commit antes de instalar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matriz de decisión: qué mirar antes de instalar
&lt;/h2&gt;

&lt;p&gt;Esto es lo que reviso, en orden, antes de sumar una dependencia nueva a un proyecto TypeScript:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Mantenimiento activo&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Último commit: menos de 6 meses es buena señal, más de 18 meses es alerta.&lt;/li&gt;
&lt;li&gt;Issues abiertos vs. cerrados: un ratio muy desbalanceado hacia abiertos sugiere que nadie triagea.&lt;/li&gt;
&lt;li&gt;Cantidad de mainteiners: uno solo es un punto único de falla — si esa persona se cansa, el proyecto muere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Superficie de la librería&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Resuelve un problema puntual o es un framework completo? Cuanto más chica la superficie, menos cosas puede romper y menos cosas hay que auditar.&lt;/li&gt;
&lt;li&gt;¿Cuántas cosas exporta que en realidad no vas a usar? Superficie no usada es riesgo sin beneficio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Tipos de TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Tiene tipos propios o depende de &lt;code&gt;@types/paquete&lt;/code&gt; separado? Los tipos separados se desincronizan de la implementación real más seguido de lo que gustaría admitir.&lt;/li&gt;
&lt;li&gt;Si trabajás con &lt;code&gt;strict: true&lt;/code&gt; y strict null checks activados, como comenté en el post sobre &lt;a href="https://juanchi.dev/es/blog/strict-null-checks-typescript-produccion" rel="noopener noreferrer"&gt;strict null checks en producción&lt;/a&gt;, una librería con tipos mal hechos te va a generar &lt;code&gt;any&lt;/code&gt; implícitos que el compilador no va a poder atajar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Transitive dependencies&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Corré &lt;code&gt;pnpm why &amp;lt;paquete&amp;gt;&lt;/code&gt; para ver qué arrastra. Una librería "liviana" puede traer quince dependencias transitivas que vos nunca elegiste.&lt;/li&gt;
&lt;li&gt;Cuantas más transitivas, más superficie de ataque y más probabilidad de que un CVE en un paquete de tercer nivel te afecte sin que lo sepas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Salida (exit strategy)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si esta librería se abandona en dos años, ¿cuánto cuesta sacarla? Si está esparcida por todo el codebase sin una capa de abstracción intermedia, el costo de salida es alto.&lt;/li&gt;
&lt;li&gt;Si es reemplazable por código propio en un día, el riesgo de dependencia baja mucho.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un chequeo rápido y reproducible para el punto 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ver el arbol de dependencias transitivas de un paquete&lt;/span&gt;
pnpm why nombre-del-paquete

&lt;span class="c"&gt;# Auditoria de vulnerabilidades conocidas&lt;/span&gt;
pnpm audit

&lt;span class="c"&gt;# Ver fecha del ultimo publish en el registro de npm&lt;/span&gt;
npm view nombre-del-paquete time.modified
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ninguno de estos comandos te da una respuesta binaria. Te dan datos para decidir con criterio, que es distinto a tener una regla automática. Ese mismo principio — evaluar con datos concretos en vez de confiar en la reputación general de una herramienta — es el que aplico cuando reviso integraciones de modelos externos, como conté al &lt;a href="https://juanchi.dev/es/blog/deepseek-api-typescript-integracion-segura" rel="noopener noreferrer"&gt;evaluar la API de DeepSeek en TypeScript&lt;/a&gt;: la pregunta nunca es "¿es buena la herramienta en general?", es "¿es buena para esto específico, con este nivel de mantenimiento?".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Necesito resolver X] --&amp;gt; B{¿Lo resuelvo en menos de un dia con codigo propio?}
  B --&amp;gt;|si| C[Escribo codigo propio]
  B --&amp;gt;|no| D{¿Mantenimiento activo y tipos propios?}
  D --&amp;gt;|no| E[Buscar alternativa o reconsiderar]
  D --&amp;gt;|si| F{¿Transitivas razonables y salida clara?}
  F --&amp;gt;|no| E
  F --&amp;gt;|si| G[Instalar con abstraccion intermedia]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Los límites de esta evaluación
&lt;/h2&gt;

&lt;p&gt;Esta matriz no reemplaza un experimento real. Todo lo de arriba es criterio de lectura previa — mirar metadata, historial, tipos — no es lo mismo que correr la librería bajo carga, medir su comportamiento en el runtime específico del proyecto, o ver cómo reacciona el mainteiner ante un issue real reportado por el propio equipo.&lt;/p&gt;

&lt;p&gt;Tampoco se puede concluir, a partir de "mantenimiento activo hoy", que la librería va a seguir manteniéndose activamente el año que viene. El historial de commits es evidencia de comportamiento pasado, no una garantía de comportamiento futuro. Proyectos con mainteiners solitarios cambian de estado de golpe — por trabajo nuevo, por burnout, por decisión personal — y no hay checklist que prediga eso con certeza.&lt;/p&gt;

&lt;p&gt;Y ojo con &lt;code&gt;npm audit&lt;/code&gt; en particular: te dice qué vulnerabilidades están reportadas y catalogadas hoy en la base de datos de npm. No te dice qué vulnerabilidades existen pero todavía no fueron reportadas, ni te dice si esa vulnerabilidad reportada aplica realmente al patrón de uso que tenés en el proyecto. Es una señal, no un veredicto.&lt;/p&gt;

&lt;p&gt;Si el proyecto es crítico — maneja datos sensibles, corre en un entorno regulado, tiene SLA de disponibilidad — esta matriz es el punto de partida, no el final. Ahí corresponde una revisión más profunda: leer el código fuente de la librería entera, no solo su README, y considerar un experimento propio con carga controlada antes de decidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo sé si una librería npm está bien mantenida?&lt;/strong&gt;&lt;br&gt;
Mirá la fecha del último commit y del último release, la cantidad de mainteiners activos, y el ratio de issues cerrados versus abiertos. Ninguno de estos datos solo es determinante, pero juntos dan una foto bastante clara.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;npm audit&lt;/code&gt; es suficiente para evaluar la seguridad de una dependencia?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;npm audit&lt;/code&gt; chequea vulnerabilidades ya reportadas y catalogadas. No detecta vulnerabilidades desconocidas ni evalúa si el patrón de uso propio te expone realmente a un CVE listado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena usar una librería sin tipos propios de TypeScript?&lt;/strong&gt;&lt;br&gt;
Depende de la superficie. Si es algo chico y con &lt;code&gt;@types&lt;/code&gt; bien mantenido, es aceptable. Si es algo central en la arquitectura, tipos propios y bien mantenidos deberían ser un requisito, no un lujo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo reviso las dependencias transitivas de un paquete antes de instalarlo?&lt;/strong&gt;&lt;br&gt;
Con &lt;code&gt;pnpm why &amp;lt;paquete&amp;gt;&lt;/code&gt; después de instalar, o revisando el &lt;code&gt;package.json&lt;/code&gt; del paquete en el registro de npm antes de decidir. También sirve mirar el tamaño del árbol con herramientas como &lt;code&gt;npm ls&lt;/code&gt; en modo dry-run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué hago si la librería que necesito está prácticamente abandonada?&lt;/strong&gt;&lt;br&gt;
Evaluá el costo de forkearla vos mismo versus escribir una alternativa propia acotada al problema real. Si la superficie que usás es chica, muchas veces conviene más código propio de diez líneas que una dependencia externa sin dueño.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿pnpm cambia algo respecto a npm en esta evaluación?&lt;/strong&gt;&lt;br&gt;
La mecánica de instalación cambia — pnpm usa un almacén de contenido compartido y es más estricto con el acceso a dependencias no declaradas — pero el criterio de evaluación de mantenimiento, tipos y superficie es el mismo independientemente del gestor de paquetes que uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mi postura
&lt;/h2&gt;

&lt;p&gt;No voy a decirte que dejes de usar librerías externas. Sería una postura ridícula viniendo de alguien que labura con Next.js, Docker y media docena de paquetes en cada proyecto. Pero cada &lt;code&gt;pnpm add&lt;/code&gt; merece la misma pregunta que le harías a una contratación: ¿quién está del otro lado, y qué pasa si desaparece?&lt;/p&gt;

&lt;p&gt;La próxima vez que estés a punto de instalar algo para resolver un problema chico, probá primero si lo podés resolver en una función propia de diez líneas. Si no podés, corré &lt;code&gt;pnpm why&lt;/code&gt; después de instalar y mirá qué trajiste puertas adentro sin haberlo decidido. Esa costumbre, más que cualquier checklist, es la que separa un proyecto que se puede mantener en tres años de uno que se convierte en arqueología.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt; &lt;a href="https://docs.npmjs.com/about-packages-and-modules" rel="noopener noreferrer"&gt;npm package documentation&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/evaluar-dependencias-npm-seguridad-mantenimiento-2" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>pnpm</category>
    </item>
    <item>
      <title>Strict Null Checks in TypeScript: What the Compiler Won't Tell You and Where It Actually Hurts in Production</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:02:12 +0000</pubDate>
      <link>https://dev.to/jtorchia/strict-null-checks-in-typescript-what-the-compiler-wont-tell-you-and-where-it-actually-hurts-in-46li</link>
      <guid>https://dev.to/jtorchia/strict-null-checks-in-typescript-what-the-compiler-wont-tell-you-and-where-it-actually-hurts-in-46li</guid>
      <description>&lt;h1&gt;
  
  
  Strict Null Checks in TypeScript: What the Compiler Won't Tell You and Where It Actually Hurts in Production
&lt;/h1&gt;

&lt;p&gt;I was reviewing a Server Action in Next.js — something that compiled without a single error, clean types, green lint — when a &lt;code&gt;Cannot read properties of undefined (reading 'id')&lt;/code&gt; hit in runtime. Three minutes of retrospective later I understood the problem: the compiler had given me the green light and I believed it. That was a mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My thesis, straight up&lt;/strong&gt;: &lt;code&gt;strict null checks&lt;/code&gt; is necessary but not sufficient. The TypeScript compiler is the first filter in the system, not the last. Real null safety comes from runtime validation at the edges of the system — and there are four concrete patterns where the compiler says OK and production says otherwise.&lt;/p&gt;

&lt;p&gt;This isn't a "turn on &lt;code&gt;strict: true&lt;/code&gt; and you're done" post. It's a map of where the compiler fails silently, using the Next.js 16 + Prisma ORM 5 + strict TypeScript stack as a concrete reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strict Null Checks in TypeScript Production: What the Flag Actually Activates
&lt;/h2&gt;

&lt;p&gt;When you enable &lt;code&gt;strict: true&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt;, TypeScript turns on a more restrictive set of checks. According to the &lt;a href="https://www.typescriptlang.org/tsconfig#strict" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;, &lt;code&gt;strict&lt;/code&gt; is a shorthand that includes, among others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strictNullChecks&lt;/code&gt; — &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; are not assignable to other types without an explicit guard.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noImplicitAny&lt;/code&gt; — no variable can be left without an inferred type.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strictFunctionTypes&lt;/code&gt; — function types are checked contravariantly.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tsconfig.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;recommended&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;strict&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; do is verify that data arriving from the outside — an API, a &lt;code&gt;JSON.parse&lt;/code&gt;, a database response, an HTTP header — actually has the shape the type declares. The compiler works with static types; runtime works with real data. Two different worlds, and the gap between them is exactly where bugs live.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 4 Patterns Where the Compiler Says OK and Runtime Blows Up Anyway
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pattern 1 — Badly Typed Assertion Functions
&lt;/h3&gt;

&lt;p&gt;Assertion functions are functions the compiler treats as type guards. If you declare them wrong, TypeScript trusts them blindly.&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="c1"&gt;// ⚠️ Assertion function that doesn't do what it promises&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;asserts&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// You forgot the throw — TypeScript won't catch this&lt;/span&gt;
  &lt;span class="c1"&gt;// The compiler still marks val as T after this call&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null value detected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// log without throw&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// After this, TypeScript believes userId is string&lt;/span&gt;
&lt;span class="c1"&gt;// But if it was null, the console.warn didn't stop the flow&lt;/span&gt;
&lt;span class="nx"&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// TypeError in runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler accepts the &lt;code&gt;asserts val is T&lt;/code&gt; contract without checking the function body. If the assertion doesn't throw, the type is lying. The fix is simple but not obvious:&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="c1"&gt;// ✅ Correct assertion function — the throw is mandatory&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;asserts&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&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="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Required value was null or undefined`&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;h3&gt;
  
  
  Pattern 2 — Libraries with Imprecise Types or Implicit &lt;code&gt;any&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Plenty of ecosystem libraries publish types in &lt;code&gt;@types/&lt;/code&gt; that don't always reflect actual return values. The most common case: a function typed as &lt;code&gt;string | undefined&lt;/code&gt; that returns &lt;code&gt;null&lt;/code&gt; in certain codepaths, or the other way around.&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="c1"&gt;// Example with a hypothetical cookie parsing library&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseCookie&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;some-cookie-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// The lib is typed as string — but it can return null at runtime&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript doesn't complain because it trusts the declared type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warning sign is when you see &lt;code&gt;as string&lt;/code&gt; scattered around, or when a library returns a broad type like &lt;code&gt;any&lt;/code&gt; or &lt;code&gt;Record&amp;lt;string, unknown&amp;gt;&lt;/code&gt;. At that point, the compiler delegates responsibility to whatever type you declare — and if that type is optimistic, you've already lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checklist for external libraries:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal in the types&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;any&lt;/code&gt; return&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Validate with Zod at point of use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outdated &lt;code&gt;@types/&lt;/code&gt; types&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Check the lib's CHANGELOG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`string&lt;/td&gt;
&lt;td&gt;undefined&lt;code&gt; when it could be &lt;/code&gt;null`&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-generated types (OpenAPI, etc.)&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;Validate at the entry boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Pattern 3 — Optional Prisma ORM 5 Relations
&lt;/h3&gt;

&lt;p&gt;This one has surprised me the most working with Prisma. When you have an optional relation in the schema — &lt;code&gt;user User?&lt;/code&gt; — Prisma types it as &lt;code&gt;User | null&lt;/code&gt;. So far so good. The problem shows up when you do an &lt;code&gt;include&lt;/code&gt; and then try to access the relation without having selected that field.&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="c1"&gt;// schema.prisma&lt;/span&gt;
&lt;span class="c1"&gt;// model Post {&lt;/span&gt;
&lt;span class="c1"&gt;//   id     Int   @id&lt;/span&gt;
&lt;span class="c1"&gt;//   author User?  @relation(fields: [authorId], references: [id])&lt;/span&gt;
&lt;span class="c1"&gt;//   authorId Int?&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ The compiler accepts this — runtime can blow up&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="c1"&gt;// No include of author&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript infers post.author as User | null | undefined&lt;/span&gt;
&lt;span class="c1"&gt;// based on the generated type — but if you didn't include it,&lt;/span&gt;
&lt;span class="c1"&gt;// author simply doesn't exist on the returned object&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// undefined at runtime, not null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma 5 generates types that reflect the schema, but not the exact shape of each query. If you don't include the relation in &lt;code&gt;include&lt;/code&gt;, the field doesn't come back in the object — and the generated type doesn't express that with enough granularity. The fix:&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="c1"&gt;// ✅ Explicit result typing with the include&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// now the type correctly includes author&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript now knows post.author can be User | null (optional relation)&lt;/span&gt;
&lt;span class="c1"&gt;// and forces you to guard it before using it&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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 practical rule: in Prisma, the generated type reflects the schema, not the query. Always make your &lt;code&gt;include&lt;/code&gt;/&lt;code&gt;select&lt;/code&gt; match what the downstream code expects to consume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 4 — JSON.parse Without Runtime Validation
&lt;/h3&gt;

&lt;p&gt;This is the most classic one and the most underestimated. &lt;code&gt;JSON.parse&lt;/code&gt; returns &lt;code&gt;any&lt;/code&gt; in TypeScript — the compiler has no idea what shape that JSON has until runtime.&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="c1"&gt;// ❌ The compiler accepts this completely&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;getConfiguration&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// returns any — TypeScript trusts the declared return type&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&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;getConfiguration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// config.timeout could be undefined, string, null — the compiler doesn't know&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NaN or TypeError at runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The solution is to validate at the boundary. &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; is the tool that fits best in this stack:&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="c1"&gt;// ✅ Validation with Zod at the external data entry point&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ConfigSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&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;getConfiguration&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="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ConfigSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws ZodError if shape doesn't match&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Now the inferred type is exactly { timeout: number; endpoint: string }&lt;/span&gt;
&lt;span class="c1"&gt;// and runtime guarantees the shape before the data reaches the rest of the code&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&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;getConfiguration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same pattern applies to Server Actions in Next.js that receive form data, to external API responses, and to any data that crosses the system boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes When Configuring Strict Null Checks
&lt;/h2&gt;

&lt;p&gt;Three mistakes show up constantly when teams enable &lt;code&gt;strict&lt;/code&gt; on an existing codebase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Turning off individual checks to make it compile&lt;/strong&gt;&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;defeats&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entire&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;purpose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;strict&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strictNullChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a check breaks too much existing code, the right path is to migrate progressively with annotated and dated &lt;code&gt;// @ts-expect-error&lt;/code&gt; comments — not to disable the flag globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Using the non-null assertion operator (&lt;code&gt;!&lt;/code&gt;) without a real guard&lt;/strong&gt;&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="c1"&gt;// ❌ The ! operator tells the compiler "trust me"&lt;/span&gt;
&lt;span class="c1"&gt;// but does zero verification at runtime&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// TypeError if user is null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;!&lt;/code&gt; in the codebase is potential technical debt. If you see more than five &lt;code&gt;!&lt;/code&gt; in a single file, that's a signal that the types aren't accurately modeling the domain's reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Confusing &lt;code&gt;strict&lt;/code&gt; in Next.js config with &lt;code&gt;strict&lt;/code&gt; in &lt;code&gt;tsconfig&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next.config.js&lt;/code&gt; has a &lt;code&gt;typescript.ignoreBuildErrors&lt;/code&gt; option that, when set to &lt;code&gt;true&lt;/code&gt;, completely bypasses the compiler during the build. The &lt;code&gt;strict&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; means nothing if the build never fails on type errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Checklist: Where to Validate and Where to Trust the Compiler
&lt;/h2&gt;

&lt;p&gt;Before deciding whether to add runtime validation or trust the static type, run through this checklist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Yes&lt;/th&gt;
&lt;th&gt;No&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does the data come from outside the process? (API, file, DB, form)&lt;/td&gt;
&lt;td&gt;Validate with Zod&lt;/td&gt;
&lt;td&gt;Compiler is enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the library have &lt;code&gt;any&lt;/code&gt; types or outdated &lt;code&gt;@types/&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;Add explicit guard&lt;/td&gt;
&lt;td&gt;Compiler is enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are you using custom assertion functions?&lt;/td&gt;
&lt;td&gt;Verify they throw&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the Prisma relation in the &lt;code&gt;include&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;Type is precise&lt;/td&gt;
&lt;td&gt;Add defensive guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the type use &lt;code&gt;!&lt;/code&gt; to suppress a null?&lt;/td&gt;
&lt;td&gt;Revisit the domain model&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb&lt;/strong&gt;: if the data crossed a system boundary (network, disk, form, environment variable), validate at runtime. If the data is internal to the process and the type was inferred by TypeScript, the compiler is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limits of This Guide
&lt;/h2&gt;

&lt;p&gt;What you can't conclude from this post without more evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many production bugs come from each pattern — that depends on the specific codebase, test coverage, and team maturity.&lt;/li&gt;
&lt;li&gt;Whether Zod is always the best option over alternatives like &lt;a href="https://valibot.dev/" rel="noopener noreferrer"&gt;Valibot&lt;/a&gt; or &lt;a href="https://arktype.io/" rel="noopener noreferrer"&gt;ArkType&lt;/a&gt; — there are bundle size and ergonomics trade-offs that deserve their own analysis.&lt;/li&gt;
&lt;li&gt;Whether these patterns apply equally in a codebase using tRPC or GraphQL with codegen — those systems have their own validation layers that change the equation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you can conclude: the four patterns are reproducible, have concrete solutions, and apply directly to the Next.js 16 + Prisma 5 + strict TypeScript stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — Strict Null Checks TypeScript Production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;With &lt;code&gt;strict: true&lt;/code&gt; enabled, can I trust there are no nulls at runtime?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;strict: true&lt;/code&gt; guarantees the compiler warns you when a type can be &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt; — but it can't verify data coming in from outside the process. Data from APIs, forms, files, and databases needs additional runtime validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Prisma ORM generate types that exactly reflect what each query returns?&lt;/strong&gt;&lt;br&gt;
Partially. Prisma 5 infers the type from the schema and from the query's &lt;code&gt;include&lt;/code&gt;/&lt;code&gt;select&lt;/code&gt;. If you don't &lt;code&gt;include&lt;/code&gt; a relation, the field won't be on the returned object — but the generated type may not express that with enough precision in all cases. The safe practice is to always make the &lt;code&gt;include&lt;/code&gt; match what downstream code consumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When does it make sense to use &lt;code&gt;// @ts-expect-error&lt;/code&gt; instead of properly fixing the type?&lt;/strong&gt;&lt;br&gt;
Only in two cases: when you're progressively migrating a legacy codebase to strict (annotated with a comment explaining why and an expected resolution date), or when you're deliberately testing an error. In stable production code, &lt;code&gt;@ts-expect-error&lt;/code&gt; without justification is technical debt with an unknown expiry date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does &lt;code&gt;JSON.parse&lt;/code&gt; always return &lt;code&gt;any&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Yes, by design. TypeScript can't know the shape of the JSON until runtime. The only way to recover a concrete type is to validate the result with a library like &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; or write manual type guards. Manual guards don't scale well; Zod scales better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are assertion functions a bad practice?&lt;/strong&gt;&lt;br&gt;
Not necessarily. They're a legitimate tool in TypeScript's type system. The problem is using them without a real &lt;code&gt;throw&lt;/code&gt; — in that case, the contract you declare isn't fulfilled at runtime and the compiler can't detect it. With a proper &lt;code&gt;throw&lt;/code&gt;, they're a clean way to do imperative narrowing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it make sense to migrate to strict null checks in a large codebase that doesn't have it?&lt;/strong&gt;&lt;br&gt;
Yes, but with a strategy. The practical approach is to enable &lt;code&gt;strict: true&lt;/code&gt; and use annotated &lt;code&gt;@ts-expect-error&lt;/code&gt; to silence existing errors, then resolve them module by module — prioritizing system boundaries first (APIs, parsers, DB adapters) — and never disable &lt;code&gt;strictNullChecks&lt;/code&gt; individually just to make it compile faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compiler Is the First Filter, Not the Last
&lt;/h2&gt;

&lt;p&gt;Working with strict TypeScript in Next.js 16 and Prisma 5 changed how I think about type safety. Not as a binary "it compiled = it's safe" but as a chain: the compiler filters static errors, runtime validation filters errors at the boundaries, and integration tests cover the rest.&lt;/p&gt;

&lt;p&gt;The four patterns in this post — assertion functions without throw, libraries with imprecise types, optional Prisma relations without include, and JSON.parse without validation — have one thing in common: they all pass the compiler and they can all fail at runtime. The difference between teams that catch these before production and those that don't is systematic: the first group puts validation at the boundary and doesn't assume the compiler solves what it can't see.&lt;/p&gt;

&lt;p&gt;My practical stance: every time data enters the system from outside, Zod or equivalent. Every assertion function with a real throw. Every Prisma include reflecting what the downstream code actually needs. And zero &lt;code&gt;!&lt;/code&gt; operators without a real guard behind them.&lt;/p&gt;

&lt;p&gt;If you're working with a TypeScript codebase that mixes strict and legacy patterns, the concrete next step is to find every &lt;code&gt;JSON.parse&lt;/code&gt; without validation and start there — it's the most common boundary and the easiest one to fix first.&lt;/p&gt;

&lt;p&gt;If you want to go deeper on system boundaries with TypeScript, I have related posts that can add context: &lt;a href="https://juanchi.dev/en/blog/deepseek-api-typescript-secure-integration-model-evaluation" rel="noopener noreferrer"&gt;DeepSeek API in TypeScript&lt;/a&gt;, &lt;a href="https://juanchi.dev/en/blog/nodejs-runtime-that-changed-backend-forever" rel="noopener noreferrer"&gt;Node.js and the event loop as a stack component&lt;/a&gt;, and &lt;a href="https://dev.to/blog/docker-healthchecks-que-miden-de-verdad"&gt;Docker healthchecks in production&lt;/a&gt; all touch on the difference between what the system promises and what it delivers.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript Handbook — Strict Mode: &lt;a href="https://www.typescriptlang.org/tsconfig#strict" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/tsconfig#strict&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zod Documentation: &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;https://zod.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/strict-null-checks-typescript-production-failures" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>produccin</category>
    </item>
    <item>
      <title>Strict null checks en TypeScript: lo que el compilador no te dice y dónde sí duele en producción</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Thu, 23 Jul 2026 12:02:07 +0000</pubDate>
      <link>https://dev.to/jtorchia/strict-null-checks-en-typescript-lo-que-el-compilador-no-te-dice-y-donde-si-duele-en-produccion-41d2</link>
      <guid>https://dev.to/jtorchia/strict-null-checks-en-typescript-lo-que-el-compilador-no-te-dice-y-donde-si-duele-en-produccion-41d2</guid>
      <description>&lt;h1&gt;
  
  
  Strict null checks en TypeScript: lo que el compilador no te dice y dónde sí duele en producción
&lt;/h1&gt;

&lt;p&gt;Estaba revisando un Server Action en Next.js — algo que compilaba sin un solo error, tipos limpios, lint verde — cuando llegó un &lt;code&gt;Cannot read properties of undefined (reading 'id')&lt;/code&gt; en runtime. Tres minutos de retrospectiva después entendí el problema: el compilador me había dado luz verde y yo lo creí. Eso fue un error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi tesis, sin rodeos&lt;/strong&gt;: &lt;code&gt;strict null checks&lt;/code&gt; es necesario pero insuficiente. El compilador de TypeScript es el primer filtro del sistema, no el último. La verdadera seguridad contra nulls viene de validación en runtime en los bordes del sistema — y hay cuatro patrones concretos donde el compilador dice OK y producción dice otra cosa.&lt;/p&gt;

&lt;p&gt;No es un post de "activá &lt;code&gt;strict: true&lt;/code&gt; y listo". Es un mapa de dónde el compilador falla en silencio, con el stack Next.js 16 + Prisma ORM 5 + TypeScript estricto como referencia concreta.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strict null checks en TypeScript producción: qué activa la flag y qué no
&lt;/h2&gt;

&lt;p&gt;Cuando habilitás &lt;code&gt;strict: true&lt;/code&gt; en el &lt;code&gt;tsconfig.json&lt;/code&gt;, TypeScript activa un conjunto de checks más restrictivos. Según la &lt;a href="https://www.typescriptlang.org/tsconfig#strict" rel="noopener noreferrer"&gt;documentación oficial&lt;/a&gt;, &lt;code&gt;strict&lt;/code&gt; es un shorthand que incluye, entre otros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;strictNullChecks&lt;/code&gt; — &lt;code&gt;null&lt;/code&gt; y &lt;code&gt;undefined&lt;/code&gt; no son asignables a otros tipos sin una guarda explícita.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noImplicitAny&lt;/code&gt; — ninguna variable puede quedarse sin tipo inferido.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;strictFunctionTypes&lt;/code&gt; — los tipos de función se verifican contravariante.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tsconfig.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuración&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;recomendada&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundler"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo que &lt;code&gt;strict&lt;/code&gt; &lt;strong&gt;no hace&lt;/strong&gt; es verificar que los datos que llegan desde el exterior —una API, un &lt;code&gt;JSON.parse&lt;/code&gt;, una respuesta de base de datos, un header HTTP— tengan la forma que el tipo declara. El compilador trabaja con tipos estáticos; runtime trabaja con datos reales. Son dos mundos distintos y la brecha entre ellos es donde aparecen los bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Los 4 patrones donde el compilador dice OK y runtime te revienta igual
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Patrón 1 — Assertion functions mal tipadas
&lt;/h3&gt;

&lt;p&gt;Las assertion functions son funciones que el compilador trata como guardas de tipo. Si las declarás mal, TypeScript confía en ellas ciegamente.&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="c1"&gt;// ⚠️ Assertion function que no hace lo que promete&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;asserts&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Olvidaste el throw — TypeScript no lo detecta&lt;/span&gt;
  &lt;span class="c1"&gt;// El compilador igual marca val como T después de esta llamada&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valor null detectado&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// log sin throw&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;obtenerUserId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Después de acá, TypeScript cree que userId es string&lt;/span&gt;
&lt;span class="c1"&gt;// Pero si era null, el console.warn no detuvo el flujo&lt;/span&gt;
&lt;span class="nx"&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// TypeError en runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El compilador acepta el contrato de &lt;code&gt;asserts val is T&lt;/code&gt; sin verificar el cuerpo de la función. Si la assertion no lanza un error, el tipo miente. La corrección es simple pero no obvia:&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="c1"&gt;// ✅ Assertion function correcta — el throw es obligatorio&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertDefined&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;asserts&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&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="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Valor requerido era null o undefined`&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;h3&gt;
  
  
  Patrón 2 — Librerías sin tipos precisos o con &lt;code&gt;any&lt;/code&gt; implícito
&lt;/h3&gt;

&lt;p&gt;Muchas librerías del ecosistema publican tipos en &lt;code&gt;@types/&lt;/code&gt; que no siempre reflejan los retornos reales. El caso más común: una función tipada como &lt;code&gt;string | undefined&lt;/code&gt; que en ciertos codepaths devuelve &lt;code&gt;null&lt;/code&gt;, o viceversa.&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="c1"&gt;// Ejemplo con una librería hipotética de parseo de cookies&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseCookie&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alguna-lib-de-cookies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// La lib está tipada como string — pero puede devolver null en runtime&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript no protesta porque confía en el tipo declarado&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La señal de alerta es cuando ves &lt;code&gt;as string&lt;/code&gt; o cuando una librería retorna un tipo amplio como &lt;code&gt;any&lt;/code&gt; o &lt;code&gt;Record&amp;lt;string, unknown&amp;gt;&lt;/code&gt;. En ese punto, el compilador delega la responsabilidad al tipo que vos declarás — y si ese tipo es optimista, perdiste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checklist para librerías externas:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Señal en los tipos&lt;/th&gt;
&lt;th&gt;Riesgo&lt;/th&gt;
&lt;th&gt;Qué hacer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retorno &lt;code&gt;any&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Alto&lt;/td&gt;
&lt;td&gt;Validar con Zod en el punto de uso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tipos en &lt;code&gt;@types/&lt;/code&gt; desactualizados&lt;/td&gt;
&lt;td&gt;Medio&lt;/td&gt;
&lt;td&gt;Revisar el CHANGELOG de la lib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`string&lt;/td&gt;
&lt;td&gt;undefined&lt;code&gt; cuando podría ser &lt;/code&gt;null`&lt;/td&gt;
&lt;td&gt;Medio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tipos generados automáticamente (OpenAPI, etc.)&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;td&gt;Validar en el borde de entrada&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Patrón 3 — Relaciones opcionales de Prisma ORM 5
&lt;/h3&gt;

&lt;p&gt;Este es el que más me ha sorprendido trabajando con Prisma. Cuando tenés una relación opcional en el schema — &lt;code&gt;user User?&lt;/code&gt; — Prisma la tipea como &lt;code&gt;User | null&lt;/code&gt;. Hasta acá bien. El problema aparece cuando hacés un &lt;code&gt;include&lt;/code&gt; y después intentás acceder a la relación sin haber guardado ese campo en el &lt;code&gt;select&lt;/code&gt;.&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="c1"&gt;// schema.prisma&lt;/span&gt;
&lt;span class="c1"&gt;// model Post {&lt;/span&gt;
&lt;span class="c1"&gt;//   id     Int   @id&lt;/span&gt;
&lt;span class="c1"&gt;//   author User?  @relation(fields: [authorId], references: [id])&lt;/span&gt;
&lt;span class="c1"&gt;//   authorId Int?&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ El compilador acepta esto — runtime puede explotar&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="c1"&gt;// Sin include de author&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript infiere post.author como User | null | undefined&lt;/span&gt;
&lt;span class="c1"&gt;// según el tipo generado — pero si no hiciste el include,&lt;/span&gt;
&lt;span class="c1"&gt;// author directamente no existe en el objeto retornado&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// undefined en runtime, no null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prisma 5 genera tipos que reflejan el schema, pero no el shape exacto de cada query. Si no incluís la relación en el &lt;code&gt;include&lt;/code&gt;, el campo no viene en el objeto — y el tipo generado no lo expresa con suficiente granularidad. La corrección:&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="c1"&gt;// ✅ Tipado explícito del resultado con el include&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ahora el tipo incluye author correctamente&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript ahora sabe que post.author puede ser User | null (relación opcional)&lt;/span&gt;
&lt;span class="c1"&gt;// y lo fuerza a que lo guardes antes de usarlo&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&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="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;La regla práctica: en Prisma, el tipo generado refleja el schema, no la query. Siempre hacé coincidir el &lt;code&gt;include&lt;/code&gt;/&lt;code&gt;select&lt;/code&gt; con lo que el código downstream espera consumir.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patrón 4 — JSON.parse sin validación de runtime
&lt;/h3&gt;

&lt;p&gt;Este es el más clásico y el que más se subestima. &lt;code&gt;JSON.parse&lt;/code&gt; retorna &lt;code&gt;any&lt;/code&gt; en TypeScript — el compilador no puede saber qué forma tiene ese JSON hasta que llegue en runtime.&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="c1"&gt;// ❌ El compilador acepta esto completamente&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;obtenerConfiguracion&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// retorna any — TypeScript confía en el tipo de retorno declarado&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&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;obtenerConfiguracion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// config.timeout podría ser undefined, string, null — el compilador no sabe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NaN o TypeError en runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La solución está en validar en el borde. &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; es la herramienta que mejor encaja en este stack:&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="c1"&gt;// ✅ Validación con Zod en el punto de entrada del dato externo&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ConfigSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&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;obtenerConfiguracion&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="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ConfigSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// lanza ZodError si el shape no coincide&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Ahora el tipo inferido es exactamente { timeout: number; endpoint: string }&lt;/span&gt;
&lt;span class="c1"&gt;// y el runtime garantiza la forma antes de que el dato llegue al resto del código&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&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;obtenerConfiguracion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seguro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El mismo patrón aplica a Server Actions en Next.js que reciben datos de formularios, a responses de APIs externas y a cualquier dato que cruce el borde del sistema.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errores comunes al configurar strict null checks
&lt;/h2&gt;

&lt;p&gt;Hay tres errores que aparecen seguido cuando equipos habilitan &lt;code&gt;strict&lt;/code&gt; en un codebase existente:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Apagar checks individuales para que compile&lt;/strong&gt;&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Esto&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;anula&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;el&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;propósito&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;de&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;strict&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strictNullChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si un check rompe demasiado código existente, el camino correcto es migrar progresivamente con &lt;code&gt;// @ts-expect-error&lt;/code&gt; anotado y fechado — no desactivar la flag globalmente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Usar non-null assertion operator (&lt;code&gt;!&lt;/code&gt;) sin guarda real&lt;/strong&gt;&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="c1"&gt;// ❌ El operador ! le dice al compilador "confiá en mí"&lt;/span&gt;
&lt;span class="c1"&gt;// pero no hace ninguna verificación en runtime&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nombre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;usuario&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nombre&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// TypeError si usuario es null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada &lt;code&gt;!&lt;/code&gt; en el codebase es una deuda técnica potencial. Si ves más de cinco &lt;code&gt;!&lt;/code&gt; en un archivo, es una señal de que los tipos no están modelando bien la realidad del dominio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Confundir que &lt;code&gt;strict&lt;/code&gt; en Next.js config y en &lt;code&gt;tsconfig&lt;/code&gt; son cosas distintas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next.config.js&lt;/code&gt; tiene una opción &lt;code&gt;typescript.ignoreBuildErrors&lt;/code&gt; que, si está en &lt;code&gt;true&lt;/code&gt;, bypassea completamente el compilador en el build. El &lt;code&gt;strict&lt;/code&gt; del &lt;code&gt;tsconfig.json&lt;/code&gt; no sirve de nada si el build nunca falla por errores de tipos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checklist de decisión: dónde validar y dónde confiar en el compilador
&lt;/h2&gt;

&lt;p&gt;Antes de decidir si agregar validación de runtime o confiar en el tipo estático, pasá por esta checklist:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pregunta&lt;/th&gt;
&lt;th&gt;Sí&lt;/th&gt;
&lt;th&gt;No&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;¿El dato viene de fuera del proceso? (API, archivo, DB, formulario)&lt;/td&gt;
&lt;td&gt;Validar con Zod&lt;/td&gt;
&lt;td&gt;El compilador alcanza&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿La librería tiene tipos &lt;code&gt;any&lt;/code&gt; o tipos de &lt;code&gt;@types/&lt;/code&gt; desactualizados?&lt;/td&gt;
&lt;td&gt;Agregar guarda explícita&lt;/td&gt;
&lt;td&gt;El compilador alcanza&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Usás assertion functions propias?&lt;/td&gt;
&lt;td&gt;Verificar que lancen &lt;code&gt;throw&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿La relación de Prisma está en el &lt;code&gt;include&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;El tipo es preciso&lt;/td&gt;
&lt;td&gt;Agregar guarda defensiva&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿El tipo usa &lt;code&gt;!&lt;/code&gt; para suprimir un null?&lt;/td&gt;
&lt;td&gt;Revisitar el modelo de dominio&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Regla de dedo&lt;/strong&gt;: si el dato cruzó un borde del sistema (red, disco, formulario, variable de entorno), validá en runtime. Si el dato es interno al proceso y el tipo fue inferido por TypeScript, el compilador alcanza.&lt;/p&gt;




&lt;h2&gt;
  
  
  Límites de esta guía
&lt;/h2&gt;

&lt;p&gt;Lo que no podés concluir de este post sin más evidencia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuántos bugs en producción vienen de cada patrón — eso depende del codebase específico, la cobertura de tests y la madurez del equipo.&lt;/li&gt;
&lt;li&gt;Si Zod es siempre la mejor opción frente a alternativas como &lt;a href="https://valibot.dev/" rel="noopener noreferrer"&gt;Valibot&lt;/a&gt; o &lt;a href="https://arktype.io/" rel="noopener noreferrer"&gt;ArkType&lt;/a&gt; — hay trade-offs de bundle size y ergonomía que merecen análisis propio.&lt;/li&gt;
&lt;li&gt;Si estos patrones aplican igual en un codebase que usa tRPC o GraphQL con codegen — esos sistemas tienen sus propias capas de validación que cambian la ecuación.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo que sí podés concluir: los cuatro patrones son reproducibles, tienen solución concreta y aplican directamente al stack Next.js 16 + Prisma 5 + TypeScript estricto.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ — strict null checks TypeScript producción
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Con &lt;code&gt;strict: true&lt;/code&gt; activado puedo confiar en que no hay nulls en runtime?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;strict: true&lt;/code&gt; garantiza que el compilador te avisa cuando un tipo puede ser &lt;code&gt;null&lt;/code&gt; o &lt;code&gt;undefined&lt;/code&gt; — pero no puede verificar los datos que entran desde afuera del proceso. Los datos de APIs, formularios, archivos y bases de datos necesitan validación en runtime adicional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Prisma ORM genera tipos que reflejan exactamente lo que retorna cada query?&lt;/strong&gt;&lt;br&gt;
Parcialmente. Prisma 5 infiere el tipo a partir del schema y del &lt;code&gt;include&lt;/code&gt;/&lt;code&gt;select&lt;/code&gt; de la query. Si no hacés &lt;code&gt;include&lt;/code&gt; de una relación, el campo no va a estar en el objeto retornado — pero el tipo generado puede no expresar eso con suficiente precisión en todos los casos. La práctica segura es hacer coincidir siempre el &lt;code&gt;include&lt;/code&gt; con lo que el código downstream consume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuándo tiene sentido usar &lt;code&gt;// @ts-expect-error&lt;/code&gt; en lugar de resolver el tipo correctamente?&lt;/strong&gt;&lt;br&gt;
Solo en dos casos: cuando estás migrando un codebase legacy a strict de forma progresiva (anotado con un comentario que explique el motivo y una fecha de resolución esperada), o cuando estás testeando un error deliberado. En código de producción estable, &lt;code&gt;@ts-expect-error&lt;/code&gt; sin justificación es una deuda técnica con fecha de vencimiento desconocida.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿&lt;code&gt;JSON.parse&lt;/code&gt; siempre retorna &lt;code&gt;any&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Sí, por diseño. TypeScript no puede saber la forma del JSON hasta runtime. La única forma de recuperar un tipo concreto es validar el resultado con una librería como &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; o escribir guardas de tipo manuales. Las guardas manuales escalan mal; Zod escala mejor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Las assertion functions son una mala práctica?&lt;/strong&gt;&lt;br&gt;
No necesariamente. Son una herramienta legítima del sistema de tipos de TypeScript. El problema es usarlas sin un &lt;code&gt;throw&lt;/code&gt; real — en ese caso, el contrato que declarás no se cumple en runtime y el compilador no puede detectarlo. Con un &lt;code&gt;throw&lt;/code&gt; correcto, son una forma limpia de narrowing imperativo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido migrar a strict null checks en un codebase grande que no lo tiene?&lt;/strong&gt;&lt;br&gt;
Sí, pero con estrategia. La forma práctica es habilitar &lt;code&gt;strict: true&lt;/code&gt; y usar &lt;code&gt;@ts-expect-error&lt;/code&gt; anotado para silenciar los errores existentes, resolverlos de a módulos priorizando los bordes del sistema primero (APIs, parsers, adapters de DB), y nunca desactivar &lt;code&gt;strictNullChecks&lt;/code&gt; individualmente para que compile más rápido.&lt;/p&gt;




&lt;h2&gt;
  
  
  El compilador es el primer filtro, no el último
&lt;/h2&gt;

&lt;p&gt;Trabajar con TypeScript estricto en Next.js 16 y Prisma 5 cambió cómo pienso la seguridad de tipos. No como un binario "compiló = seguro" sino como una cadena: el compilador filtra los errores estáticos, la validación de runtime filtra los errores en los bordes, y los tests de integración cubren el resto.&lt;/p&gt;

&lt;p&gt;Los cuatro patrones de este post — assertion functions sin throw, librerías con tipos imprecisos, relaciones opcionales de Prisma sin include, y JSON.parse sin validación — tienen algo en común: todos pasan el compilador y todos pueden fallar en runtime. La diferencia entre los equipos que los atrapa antes de producción y los que no es sistemática: los primeros ponen validación en el borde y no asumen que el compilador resuelve lo que no puede ver.&lt;/p&gt;

&lt;p&gt;Mi postura práctica: cada vez que un dato entra al sistema desde afuera, Zod o equivalente. Cada assertion function con un throw real. Cada include de Prisma reflejando lo que el código downstream necesita. Y cero operadores &lt;code&gt;!&lt;/code&gt; sin guarda real detrás.&lt;/p&gt;

&lt;p&gt;Si trabajás con una codebase TypeScript que mezcla strict y patrones legacy, el siguiente paso concreto es buscar todos los &lt;code&gt;JSON.parse&lt;/code&gt; sin validación y empezar ahí — es el borde más común y el más fácil de resolver primero.&lt;/p&gt;

&lt;p&gt;Si te interesa profundizar en los bordes del sistema con TypeScript, tengo posts relacionados que pueden sumar contexto: &lt;a href="https://juanchi.dev/es/blog/deepseek-api-typescript-integracion-segura" rel="noopener noreferrer"&gt;DeepSeek API en TypeScript&lt;/a&gt;, &lt;a href="https://juanchi.dev/es/blog/nodejs-runtime-javascript-backend-event-loop-ecosystem" rel="noopener noreferrer"&gt;Node.js y el event loop como pieza del stack&lt;/a&gt; y &lt;a href="https://dev.to/blog/docker-healthchecks-que-miden-de-verdad"&gt;Docker healthchecks en producción&lt;/a&gt; también tocan la diferencia entre lo que el sistema promete y lo que entrega.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuentes originales:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript Handbook — Strict Mode: &lt;a href="https://www.typescriptlang.org/tsconfig#strict" rel="noopener noreferrer"&gt;https://www.typescriptlang.org/tsconfig#strict&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zod Documentation: &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;https://zod.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/strict-null-checks-typescript-produccion" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>DeepSeek API in TypeScript: secure integration and honest model evaluation for code</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:00:15 +0000</pubDate>
      <link>https://dev.to/jtorchia/deepseek-api-in-typescript-secure-integration-and-honest-model-evaluation-for-code-4m90</link>
      <guid>https://dev.to/jtorchia/deepseek-api-in-typescript-secure-integration-and-honest-model-evaluation-for-code-4m90</guid>
      <description>&lt;h1&gt;
  
  
  DeepSeek API in TypeScript: secure integration and honest model evaluation for code
&lt;/h1&gt;

&lt;p&gt;For months I was convinced that integrating a new model into a TypeScript pipeline was the hard part. Then I realized it never was. The hard part is deciding whether that model is actually worth it for what you need — without buying the hype or trashing it because Twitter moved on. I learned that lesson again with DeepSeek.&lt;/p&gt;

&lt;p&gt;My thesis before starting: DeepSeek's API is compatible with the OpenAI SDK, which makes integration almost trivial in any existing TypeScript pipeline. The real differentiator isn't the plumbing — it's the model. DeepSeek-Coder is competitive for code tasks, but the decision criterion depends on your specific use case, not on Twitter enthusiasm.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the official docs say — and what they don't
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://platform.deepseek.com/api-docs/" rel="noopener noreferrer"&gt;official DeepSeek documentation&lt;/a&gt; has two facts that completely change the integration conversation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenAI SDK compatibility&lt;/strong&gt;: DeepSeek exposes its API under the same message format as OpenAI. That means if you're already using the &lt;code&gt;openai&lt;/code&gt; npm package in a TypeScript pipeline, you can point it at DeepSeek's base URL with minimal changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available models&lt;/strong&gt;: As of this post, the main models are &lt;code&gt;deepseek-chat&lt;/code&gt; (general purpose) and &lt;code&gt;deepseek-coder&lt;/code&gt; (code-focused). The docs list the base endpoint as &lt;code&gt;https://api.deepseek.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What the documentation &lt;strong&gt;doesn't say&lt;/strong&gt;: independent benchmarks, real production latency comparisons, or SLA guarantees. That's your own work — or someone willing to run the experiment under real load. I'm not going to make up those numbers here.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to integrate in TypeScript without exposing the API key
&lt;/h2&gt;

&lt;p&gt;Core decision: the DeepSeek API key, like any LLM provider credential, cannot live on the client. Ever. In Next.js App Router that has a concrete answer: the logic that calls the API lives in a Route Handler (server-side), and the key travels exclusively via server environment variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: environment variable in &lt;code&gt;.env.local&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.local — NEVER commit this file&lt;/span&gt;
&lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;.gitignore&lt;/code&gt; if it isn't already. On Railway, Vercel, or any deploy platform, you configure the variable from the dashboard — never from the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: TypeScript client with OpenAI SDK compatibility
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/deepseek-client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Instance pointing to DeepSeek's endpoint&lt;/span&gt;
&lt;span class="c1"&gt;// Compatible with openai@^4 — same type contract&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// only available server-side&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.deepseek.com&lt;/span&gt;&lt;span class="dl"&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="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is in &lt;code&gt;baseURL&lt;/code&gt;: the OpenAI SDK accepts endpoint override, and DeepSeek respects the same message contract. You don't need a proprietary SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Route Handler in Next.js App Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/code-review/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/deepseek-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&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="c1"&gt;// Minimal validation before calling the model&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="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&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="nx"&gt;NextResponse&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="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="nx"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek-coder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// code-focused model&lt;/span&gt;
    &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Review the code and flag concrete issues with justification.&lt;/span&gt;&lt;span class="dl"&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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="nx"&gt;NextResponse&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="na"&gt;review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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;The client never sees the key. The browser calls &lt;code&gt;/api/code-review&lt;/code&gt;; the Route Handler calls DeepSeek. That's the pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where people get it wrong — and what it costs
&lt;/h2&gt;

&lt;p&gt;There are three common mistakes that show up in quick LLM API integrations. I'm listing them as practical criteria, because the patterns are reproducible even if the specific experience is generic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: exposing the key on the client&lt;/strong&gt;&lt;br&gt;
The typical case is a dev who copies the documentation snippet directly into a React component. &lt;code&gt;process.env.DEEPSEEK_API_KEY&lt;/code&gt; on the client is &lt;code&gt;undefined&lt;/code&gt; in Next.js by default — but if someone prefixes the variable with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, it gets exposed in the browser bundle. Cost: the key is accessible in DevTools and in any scraper that inspects the public JS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: treating &lt;code&gt;deepseek-chat&lt;/code&gt; and &lt;code&gt;deepseek-coder&lt;/code&gt; as synonyms&lt;/strong&gt;&lt;br&gt;
They're different models with different biases. &lt;code&gt;deepseek-coder&lt;/code&gt; was trained specifically for code generation and review tasks; &lt;code&gt;deepseek-chat&lt;/code&gt; is more general. Using the wrong model doesn't break the API — it breaks the quality of the response. The documentation distinguishes them explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: assuming OpenAI SDK compatibility is total&lt;/strong&gt;&lt;br&gt;
The compatibility is at the message format and response structure level. It doesn't mean DeepSeek supports every OpenAI API feature: function calling, embeddings, fine-tuning, and advanced tooling may have differences or limitations. Before assuming full parity, check the DeepSeek documentation for the specific feature you need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision matrix: DeepSeek-Coder vs Claude for code tasks
&lt;/h2&gt;

&lt;p&gt;This is the part where most posts hand you a winner and call it done. I'm not going to do that — because the honest answer depends on variables I can't measure for you.&lt;/p&gt;

&lt;p&gt;What I can give you is the decision framework:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;DeepSeek-Coder&lt;/th&gt;
&lt;th&gt;Claude (Sonnet/Opus)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lower as of publication date&lt;/td&gt;
&lt;td&gt;Higher on powerful models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check official documentation&lt;/td&gt;
&lt;td&gt;Claude has 200k tokens on Opus/Sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI SDK integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native, same contract&lt;/td&gt;
&lt;td&gt;Requires Anthropic SDK or wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-step reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Competitive on code&lt;/td&gt;
&lt;td&gt;Stronger on general reasoning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability / uptime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Newer provider, shorter track record&lt;/td&gt;
&lt;td&gt;Anthropic has a longer track record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content restrictions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Less detailed documentation&lt;/td&gt;
&lt;td&gt;Better documented and more predictable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When it's worth trying DeepSeek-Coder first:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The pipeline is exclusively code generation or review&lt;/li&gt;
&lt;li&gt;API cost is a relevant variable in the design&lt;/li&gt;
&lt;li&gt;You're already on the OpenAI SDK and want minimal friction to evaluate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to stick with Claude:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need multi-step reasoning or very long context&lt;/li&gt;
&lt;li&gt;Model behavior predictability matters more than cost&lt;/li&gt;
&lt;li&gt;The pipeline mixes code tasks with general reasoning or analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you can't decide without your own experiment:&lt;/strong&gt; perceived response speed in production, quality on your specific code domain, and behavior under load. That data doesn't exist in any post — it exists in your own logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this guide can't conclude
&lt;/h2&gt;

&lt;p&gt;Being honest here is part of the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No first-party benchmarks&lt;/strong&gt;: I didn't run systematic comparisons between DeepSeek-Coder and Claude against real use cases. The public benchmarks circulating out there have different methodologies and aren't always reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DeepSeek's documentation can change&lt;/strong&gt;: it's an actively growing platform. What's available today may change. Always check &lt;code&gt;https://platform.deepseek.com/api-docs/&lt;/code&gt; before making architecture decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI SDK compatibility is not a parity guarantee&lt;/strong&gt;: it's an entry point, not a complete contract. Test the specific feature you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative API costs fluctuate&lt;/strong&gt;: don't anchor architecture decisions to pricing numbers that change every quarter.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ — Common questions about DeepSeek API in TypeScript
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need a special SDK to use DeepSeek in TypeScript?&lt;/strong&gt;&lt;br&gt;
No. You can use the official &lt;code&gt;openai&lt;/code&gt; npm package pointing &lt;code&gt;baseURL&lt;/code&gt; at &lt;code&gt;https://api.deepseek.com&lt;/code&gt;. DeepSeek respects the same message format, so the OpenAI SDK's TypeScript types work without modifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the real difference between &lt;code&gt;deepseek-chat&lt;/code&gt; and &lt;code&gt;deepseek-coder&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
According to the official documentation, &lt;code&gt;deepseek-coder&lt;/code&gt; was trained specifically for code tasks: generation, explanation, debugging, and review. &lt;code&gt;deepseek-chat&lt;/code&gt; is the general-purpose model. For a code-focused pipeline, &lt;code&gt;deepseek-coder&lt;/code&gt; is the logical starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I protect the API key in a Next.js project?&lt;/strong&gt;&lt;br&gt;
The key lives in &lt;code&gt;.env.local&lt;/code&gt; (never in the repository) and is used exclusively in server-side code: Route Handlers or Server Actions. Never prefix the variable with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; — that exposes it in the browser bundle. In production, configure it from your deploy platform's dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use DeepSeek and Claude in the same pipeline?&lt;/strong&gt;&lt;br&gt;
Yes, and it's a reasonable pattern: use DeepSeek-Coder for mechanical code tasks (boilerplate generation, conversions, snippets) and Claude for more complex reasoning or long context. The router between models is logic you write yourself. This connects to the same design decision that comes up in &lt;a href="https://juanchi.dev/en/blog/rate-limiting-web-apps-what-to-protect-before-picking-library" rel="noopener noreferrer"&gt;rate limiting in web applications&lt;/a&gt;: deciding which layer you protect and with what tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does OpenAI SDK compatibility guarantee all features will work the same?&lt;/strong&gt;&lt;br&gt;
No. Compatibility is at the basic chat completions level. Features like function calling, embeddings, batch API, or fine-tuning may have differences or simply not be available in DeepSeek. Before assuming parity, verify the specific feature you need in the official documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it make sense to use DeepSeek in a pipeline that already uses Claude or GPT-4?&lt;/strong&gt;&lt;br&gt;
Depends on the case. If API cost is relevant and the tasks are mechanical (repetitive code generation, formatting, short snippets), it's worth evaluating. If the pipeline depends on multi-step reasoning or very long context, the switch may degrade response quality. The honest decision comes from running the experiment in your own domain, not from general benchmarks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real decision, no decoration
&lt;/h2&gt;

&lt;p&gt;Integrating DeepSeek in TypeScript is easy — intentionally easy. The OpenAI SDK compatibility is a product decision that brings adoption friction down to nearly zero. That's a real advantage and it deserves acknowledgment.&lt;/p&gt;

&lt;p&gt;What isn't easy is the model decision. And here my position is clear: I'm not buying anyone's claim that DeepSeek-Coder is better than Claude for code "in general" — because "in general" doesn't exist in production. What exists is the specific domain, the type of task, the token volume, and the project budget.&lt;/p&gt;

&lt;p&gt;What I do accept as a starting point: if you already have a pipeline on the OpenAI SDK and want to evaluate DeepSeek-Coder, the cost of the test is minimal. Change the &lt;code&gt;baseURL&lt;/code&gt;, change the model, run the same set of prompts you already have, and look at the results. That's the only honest way to compare.&lt;/p&gt;

&lt;p&gt;Twitter hype doesn't replace that experiment. Neither do I.&lt;/p&gt;

&lt;p&gt;If pipeline architecture interests you, the post on &lt;a href="https://juanchi.dev/en/blog/nodejs-runtime-that-changed-backend-forever" rel="noopener noreferrer"&gt;Node.js and the event loop&lt;/a&gt; has useful context on how to think about the runtime behind these integrations. And if you're thinking about how to protect these endpoints before exposing them, &lt;a href="https://juanchi.dev/en/blog/rate-limiting-web-apps-what-to-protect-before-picking-library" rel="noopener noreferrer"&gt;the rate limiting post&lt;/a&gt; is the next step.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Original source:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek API Documentation: &lt;a href="https://platform.deepseek.com/api-docs/" rel="noopener noreferrer"&gt;https://platform.deepseek.com/api-docs/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://juanchi.dev/en/blog/deepseek-api-typescript-secure-integration-model-evaluation" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>english</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>llm</category>
    </item>
    <item>
      <title>DeepSeek API en TypeScript: integración segura y evaluación honesta del modelo para código</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:00:10 +0000</pubDate>
      <link>https://dev.to/jtorchia/deepseek-api-en-typescript-integracion-segura-y-evaluacion-honesta-del-modelo-para-codigo-591a</link>
      <guid>https://dev.to/jtorchia/deepseek-api-en-typescript-integracion-segura-y-evaluacion-honesta-del-modelo-para-codigo-591a</guid>
      <description>&lt;h1&gt;
  
  
  DeepSeek API en TypeScript: integración segura y evaluación honesta del modelo para código
&lt;/h1&gt;

&lt;p&gt;Estuve meses convencido de que integrar un modelo nuevo al pipeline de TypeScript era la parte difícil. Después me di cuenta de que nunca lo fue. La parte difícil es decidir si ese modelo vale para lo que necesitás — sin comprar el hype ni descartarlo por moda. Con DeepSeek lo aprendí de nuevo.&lt;/p&gt;

&lt;p&gt;Mi tesis antes de arrancar: la API de DeepSeek es compatible con el SDK de OpenAI, lo que hace la integración casi trivial en cualquier pipeline TypeScript existente. El diferenciador real no está en la plomería — está en el modelo. DeepSeek-Coder es competitivo en tareas de código, pero el criterio de elección depende del caso de uso específico, no del entusiasmo de Twitter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qué dice la documentación oficial — y qué no dice
&lt;/h2&gt;

&lt;p&gt;La &lt;a href="https://platform.deepseek.com/api-docs/" rel="noopener noreferrer"&gt;documentación oficial de DeepSeek&lt;/a&gt; tiene dos datos que cambian completamente la conversación sobre integración:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibilidad con el SDK de OpenAI&lt;/strong&gt;: DeepSeek expone su API bajo el mismo formato de mensajes que OpenAI. Eso significa que si ya usás &lt;code&gt;openai&lt;/code&gt; npm package en un pipeline TypeScript, podés apuntar a la base URL de DeepSeek con mínimos cambios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modelos disponibles&lt;/strong&gt;: A la fecha de este post, los modelos principales son &lt;code&gt;deepseek-chat&lt;/code&gt; (propósito general) y &lt;code&gt;deepseek-coder&lt;/code&gt; (orientado a código). La documentación lista el endpoint base como &lt;code&gt;https://api.deepseek.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lo que la documentación &lt;strong&gt;no dice&lt;/strong&gt;: benchmarks independientes, comparaciones de latencia en producción real, ni garantías de SLA. Eso es trabajo propio — o de alguien que quiera correr el experimento con carga real. Yo no voy a inventar esos números acá.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cómo se integra en TypeScript sin exponer la API key
&lt;/h2&gt;

&lt;p&gt;Spine de la decisión: la API key de DeepSeek, como cualquier credential de un proveedor LLM, no puede vivir en el cliente. Nunca. En Next.js App Router eso tiene una respuesta concreta: la lógica que llama a la API vive en un Route Handler (server-side), y la key viaja exclusivamente via variable de entorno del servidor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 1: variable de entorno en &lt;code&gt;.env.local&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.local — NUNCA commitear este archivo&lt;/span&gt;
&lt;span class="nv"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agregalo a &lt;code&gt;.gitignore&lt;/code&gt; si no está. En Railway, Vercel o cualquier plataforma de deploy, configurás la variable desde el panel — nunca desde el repositorio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 2: cliente TypeScript con compatibilidad OpenAI SDK
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/deepseek-client.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OpenAI&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Instancia apuntando al endpoint de DeepSeek&lt;/span&gt;
&lt;span class="c1"&gt;// Compatible con openai@^4 — mismo contrato de tipos&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// solo disponible server-side&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.deepseek.com&lt;/span&gt;&lt;span class="dl"&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="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La clave está en &lt;code&gt;baseURL&lt;/code&gt;: el SDK de OpenAI acepta override del endpoint, y DeepSeek respeta el mismo contrato de mensajes. No necesitás un SDK propietario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 3: Route Handler en Next.js App Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/code-review/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/deepseek-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&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="c1"&gt;// Validación mínima antes de llamar al modelo&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="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&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="nx"&gt;NextResponse&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payload inválido&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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="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="nx"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;deepseek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deepseek-coder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// modelo orientado a código&lt;/span&gt;
    &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Revisá el código y señalá problemas concretos con justificación.&lt;/span&gt;&lt;span class="dl"&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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="nx"&gt;NextResponse&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="na"&gt;review&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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;El cliente nunca ve la key. El browser llama a &lt;code&gt;/api/code-review&lt;/code&gt;; el Route Handler llama a DeepSeek. Ese es el patrón.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dónde se equivoca la gente — y cuánto cuesta
&lt;/h2&gt;

&lt;p&gt;Hay tres errores comunes que aparecen en integraciones rápidas de APIs LLM. Los listo como criterio prudente, porque los patrones son reproducibles aunque la experiencia sea genérica:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 1: exponer la key en el cliente&lt;/strong&gt;&lt;br&gt;
El caso típico es un dev que copia el snippet de la documentación directamente en un componente React. &lt;code&gt;process.env.DEEPSEEK_API_KEY&lt;/code&gt; en el cliente es &lt;code&gt;undefined&lt;/code&gt; en Next.js por defecto — pero si alguien prefija la variable con &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, la expone en el bundle del browser. Costo: la key queda accesible en DevTools y en cualquier scraper que revise el JS público.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: tratar &lt;code&gt;deepseek-chat&lt;/code&gt; y &lt;code&gt;deepseek-coder&lt;/code&gt; como sinónimos&lt;/strong&gt;&lt;br&gt;
Son modelos distintos con sesgos distintos. &lt;code&gt;deepseek-coder&lt;/code&gt; fue entrenado específicamente para tareas de generación y revisión de código; &lt;code&gt;deepseek-chat&lt;/code&gt; es más general. Usar el modelo equivocado no rompe la API — rompe la calidad de la respuesta. La documentación los distingue explícitamente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: asumir que la compatibilidad con OpenAI SDK es total&lt;/strong&gt;&lt;br&gt;
La compatibilidad es a nivel de formato de mensajes y estructura de respuesta. No significa que DeepSeek soporte todas las features del API de OpenAI: function calling, embeddings, fine-tuning y herramientas avanzadas pueden tener diferencias o limitaciones. Antes de asumir paridad completa, revisá la documentación de DeepSeek para el feature específico que necesitás.&lt;/p&gt;




&lt;h2&gt;
  
  
  Matriz de decisión: DeepSeek-Coder vs Claude para tareas de código
&lt;/h2&gt;

&lt;p&gt;Esta es la parte donde la mayoría de posts te da un winner y cierra el tema. Yo no voy a hacer eso — porque la respuesta honesta depende de variables que no puedo medir por vos.&lt;/p&gt;

&lt;p&gt;Lo que sí puedo darte es el criterio de decisión:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterio&lt;/th&gt;
&lt;th&gt;DeepSeek-Coder&lt;/th&gt;
&lt;th&gt;Claude (Sonnet/Opus)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Costo de API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Más bajo a fecha de publicación&lt;/td&gt;
&lt;td&gt;Más alto en modelos potentes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Contexto largo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Revisar documentación oficial&lt;/td&gt;
&lt;td&gt;Claude tiene 200k tokens en Opus/Sonnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integración con SDK OpenAI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nativa, mismo contrato&lt;/td&gt;
&lt;td&gt;Requiere SDK de Anthropic o wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Razonamiento multi-paso&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Competitivo en código&lt;/td&gt;
&lt;td&gt;Más fuerte en razonamiento general&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disponibilidad / uptime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proveedor más nuevo, historial más corto&lt;/td&gt;
&lt;td&gt;Anthropic tiene historial más largo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Restricciones de contenido&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Documentación menos detallada&lt;/td&gt;
&lt;td&gt;Más documentada y predecible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cuándo vale probar DeepSeek-Coder primero:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El pipeline es exclusivamente de generación o revisión de código&lt;/li&gt;
&lt;li&gt;El costo de API es una variable relevante en el diseño&lt;/li&gt;
&lt;li&gt;Ya usás el SDK de OpenAI y querés mínima fricción para probar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cuándo quedarse con Claude:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necesitás razonamiento multi-paso o contexto muy largo&lt;/li&gt;
&lt;li&gt;La predictibilidad del comportamiento del modelo importa más que el costo&lt;/li&gt;
&lt;li&gt;El pipeline mezcla tareas de código con razonamiento general o análisis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lo que no podés decidir sin vos propio experimento:&lt;/strong&gt; velocidad de respuesta percibida en producción, calidad en el dominio específico del código que generás, y comportamiento bajo carga. Esos datos no existen en ningún post — existen en logs propios.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que esta guía no puede concluir
&lt;/h2&gt;

&lt;p&gt;Ser honesto acá es parte del trabajo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No hay benchmarks propios&lt;/strong&gt;: no corrí comparaciones sistemáticas entre DeepSeek-Coder y Claude con casos de uso reales. Los benchmarks públicos que circulan tienen metodologías distintas y no siempre son reproducibles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La documentación de DeepSeek puede cambiar&lt;/strong&gt;: es una plataforma en crecimiento activo. Lo que está disponible hoy puede cambiar. Revisá siempre &lt;code&gt;https://platform.deepseek.com/api-docs/&lt;/code&gt; antes de tomar decisiones de arquitectura.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La compatibilidad con OpenAI SDK no es garantía de paridad&lt;/strong&gt;: es un punto de entrada, no un contrato completo. Testeá el feature específico que necesitás.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El costo relativo de las APIs fluctúa&lt;/strong&gt;: no pongas decisiones de arquitectura en números de pricing que cambian cada trimestre.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ — Preguntas frecuentes sobre DeepSeek API en TypeScript
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Necesito un SDK especial para usar DeepSeek en TypeScript?&lt;/strong&gt;&lt;br&gt;
No. Podés usar el paquete oficial &lt;code&gt;openai&lt;/code&gt; de npm apuntando el &lt;code&gt;baseURL&lt;/code&gt; a &lt;code&gt;https://api.deepseek.com&lt;/code&gt;. DeepSeek respeta el mismo formato de mensajes, así que el tipado TypeScript del SDK de OpenAI funciona sin modificaciones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es la diferencia real entre &lt;code&gt;deepseek-chat&lt;/code&gt; y &lt;code&gt;deepseek-coder&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Según la documentación oficial, &lt;code&gt;deepseek-coder&lt;/code&gt; fue entrenado específicamente para tareas de código: generación, explicación, debugging y revisión. &lt;code&gt;deepseek-chat&lt;/code&gt; es el modelo de propósito general. Para un pipeline enfocado en código, &lt;code&gt;deepseek-coder&lt;/code&gt; es el punto de partida lógico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo protejo la API key en un proyecto Next.js?&lt;/strong&gt;&lt;br&gt;
La key vive en &lt;code&gt;.env.local&lt;/code&gt; (nunca en el repositorio) y se usa exclusivamente en código server-side: Route Handlers o Server Actions. Nunca prefijés la variable con &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; porque eso la expone en el bundle del browser. En producción, configurala desde el panel de la plataforma de deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puedo usar DeepSeek y Claude en el mismo pipeline?&lt;/strong&gt;&lt;br&gt;
Sí, y es un patrón razonable: usar DeepSeek-Coder para tareas mecánicas de código (generación de boilerplate, conversiones, snippets) y Claude para razonamiento más complejo o contexto largo. El router entre modelos es lógica que escribís vos. Esto conecta con la misma decisión de diseño que aparece en &lt;a href="https://juanchi.dev/es/blog/rate-limiting-aplicaciones-web-nextjs-2" rel="noopener noreferrer"&gt;rate limiting en aplicaciones web&lt;/a&gt;: decidir qué capa protegés y con qué herramienta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿La compatibilidad con OpenAI SDK garantiza que todas las features van a funcionar igual?&lt;/strong&gt;&lt;br&gt;
No. La compatibilidad es a nivel de chat completions básico. Features como function calling, embeddings, batch API o fine-tuning pueden tener diferencias o directamente no estar disponibles en DeepSeek. Antes de asumir paridad, verificá en la documentación oficial el feature específico que necesitás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido usar DeepSeek en un pipeline que ya usa Claude o GPT-4?&lt;/strong&gt;&lt;br&gt;
Depende del caso. Si el costo de API es relevante y las tareas son mecánicas (generación de código repetitivo, formateo, snippets cortos), vale evaluarlo. Si el pipeline depende de razonamiento multi-paso o contexto muy largo, el cambio puede deteriorar la calidad de las respuestas. La decisión honesta viene de correr el experimento en el propio dominio, no de benchmarks generales.&lt;/p&gt;




&lt;h2&gt;
  
  
  La decisión real, sin adornos
&lt;/h2&gt;

&lt;p&gt;La integración de DeepSeek en TypeScript es fácil — intencionalmente fácil. La compatibilidad con el SDK de OpenAI es una decisión de producto que baja la fricción de adopción a casi cero. Eso es una ventaja real y vale reconocerla.&lt;/p&gt;

&lt;p&gt;Lo que no es fácil es la decisión de modelo. Y acá mi postura es clara: no le compro a nadie la idea de que DeepSeek-Coder es mejor que Claude para código "en general" — porque "en general" no existe en producción. Existe el dominio específico, el tipo de tarea, el volumen de tokens y el presupuesto del proyecto.&lt;/p&gt;

&lt;p&gt;Lo que sí acepto como punto de partida: si ya tenés un pipeline con el SDK de OpenAI y querés evaluar DeepSeek-Coder, el costo de la prueba es mínimo. Cambiás el &lt;code&gt;baseURL&lt;/code&gt;, cambiás el modelo, corrés el mismo conjunto de prompts que ya tenés y mirás los resultados. Esa es la única forma honesta de comparar.&lt;/p&gt;

&lt;p&gt;El hype de Twitter no reemplaza ese experimento. Yo tampoco.&lt;/p&gt;

&lt;p&gt;Si el tema de arquitectura de pipelines te interesa, el post sobre &lt;a href="https://juanchi.dev/es/blog/nodejs-runtime-javascript-backend-event-loop-ecosystem" rel="noopener noreferrer"&gt;Node.js y el event loop&lt;/a&gt; tiene contexto útil sobre cómo pensar el runtime detrás de estas integraciones. Y si estás pensando en cómo proteger estos endpoints antes de exponerlos, &lt;a href="https://juanchi.dev/es/blog/rate-limiting-aplicaciones-web-nextjs-2" rel="noopener noreferrer"&gt;el post de rate limiting&lt;/a&gt; es el paso siguiente.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Fuente original:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DeepSeek API Documentation: &lt;a href="https://platform.deepseek.com/api-docs/" rel="noopener noreferrer"&gt;https://platform.deepseek.com/api-docs/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/es/blog/deepseek-api-typescript-integracion-segura" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>espanol</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
