<?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: Alberto García</title>
    <description>The latest articles on DEV Community by Alberto García (@alberto_garca_45e0d97494).</description>
    <link>https://dev.to/alberto_garca_45e0d97494</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%2F3453037%2Fa0d55de0-b6bb-42d2-a6b5-34e9041a7587.jpg</url>
      <title>DEV Community: Alberto García</title>
      <link>https://dev.to/alberto_garca_45e0d97494</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alberto_garca_45e0d97494"/>
    <language>en</language>
    <item>
      <title>Your email regex is not email verification</title>
      <dc:creator>Alberto García</dc:creator>
      <pubDate>Wed, 22 Jul 2026 10:54:59 +0000</pubDate>
      <link>https://dev.to/alberto_garca_45e0d97494/your-email-regex-is-not-email-verification-4h6j</link>
      <guid>https://dev.to/alberto_garca_45e0d97494/your-email-regex-is-not-email-verification-4h6j</guid>
      <description>&lt;p&gt;Six months ago I needed to stop fake signups on a side project. The plan was simple: check the email at signup, reject the bad ones, move on with my life.&lt;/p&gt;

&lt;p&gt;That turned into building &lt;a href="https://mailbeam.dev/" rel="noopener noreferrer"&gt;Mailbeam&lt;/a&gt;, because every step of "simple" turned out to be a trapdoor.&lt;/p&gt;

&lt;p&gt;This is the write-up I wish I'd found before I started. It's mostly about the parts that broke.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: the regex, which is a lie
&lt;/h2&gt;

&lt;p&gt;Everyone starts here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you the string is shaped like an email address. That's it. It happily accepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;asdf@asdf.com&lt;/code&gt; — valid syntax, nobody home&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user@gmial.com&lt;/code&gt; — valid syntax, typo'd domain, your welcome email bounces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;test@mailinator.com&lt;/code&gt; — valid syntax, disposable, gone in 10 minutes
If you go the other way and try to actually implement RFC 5322, you'll find the official grammar permits things like &lt;code&gt;"very.(),:;&amp;lt;&amp;gt;[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com&lt;/code&gt;. There's a &lt;a href="https://stackoverflow.com/questions/201323" rel="noopener noreferrer"&gt;famous 6,000-character regex&lt;/a&gt; that gets close to correct and is useless in practice, because RFC-valid and deliverable are completely different questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Syntax validation catches maybe 5% of your bad signups. Necessary, nowhere near sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: MX records, the actual first useful check
&lt;/h2&gt;

&lt;p&gt;Before you can deliver mail to a domain, that domain needs a mail server. You can check this in one DNS lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dns&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;node:dns&lt;/span&gt;&lt;span class="dl"&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;hasMailServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;try&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;records&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;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveMx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&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;records&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;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NXDOMAIN or no MX&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hasMailServer&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@gmial.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// depends on the day — typosquatters buy these&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hasMailServer&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@notarealdomain-zzz.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is cheap, fast, and catches a real chunk of garbage. It's also where most homegrown implementations stop, and it's the point where you start believing the problem is solved.&lt;/p&gt;

&lt;p&gt;Two gotchas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback to A records.&lt;/strong&gt; RFC 5321 says that if a domain has no MX record, mail should fall back to its A record. Most naive implementations don't check this and reject valid domains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typosquatting.&lt;/strong&gt; &lt;code&gt;gmial.com&lt;/code&gt;, &lt;code&gt;hotmial.com&lt;/code&gt;, &lt;code&gt;yaho.com&lt;/code&gt; — a lot of these are registered, with MX records, by people who want your users' password reset emails. An MX check passes them. You need a separate Levenshtein-distance check against a list of common providers to catch typos, and then you have to decide whether to hard-reject or just warn the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: SMTP probing, where it all falls apart
&lt;/h2&gt;

&lt;p&gt;To know whether a specific &lt;em&gt;mailbox&lt;/em&gt; exists, the theory is you open an SMTP conversation with the mail server and ask, without sending anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;gt; HELO yourdomain.com
&amp;lt; 250 mx.example.com
&amp;gt; MAIL FROM: &amp;lt;check@yourdomain.com&amp;gt;
&amp;lt; 250 OK
&amp;gt; RCPT TO: &amp;lt;user@example.com&amp;gt;
&amp;lt; 250 OK          ← mailbox exists
&amp;lt; 550 No such user ← it doesn't
&amp;gt; QUIT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, right? Here's what happens when you actually run it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your cloud provider blocks port 25.&lt;/strong&gt; AWS blocks outbound port 25 on EC2 by default and requires you to file a request to lift it. Google Cloud blocks outbound port 25 entirely, with no exception process. So the moment you deploy, your verification silently starts returning "unknown" for everything. Discovering this in production is a genuinely bad afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greylisting.&lt;/strong&gt; A large share of mail servers deliberately return a temporary &lt;code&gt;4xx&lt;/code&gt; to unknown senders on first contact, expecting a legitimate sender to retry in a few minutes. A real MTA retries. Your synchronous signup endpoint cannot wait four minutes. You get a soft failure and no information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Servers lie on purpose.&lt;/strong&gt; Accepting every &lt;code&gt;RCPT TO&lt;/code&gt; is standard anti-enumeration hygiene. If a server returned &lt;code&gt;550&lt;/code&gt; for nonexistent mailboxes, anyone could harvest its entire valid address list in an afternoon. So many servers — including a lot of Microsoft 365 tenants — return &lt;code&gt;250&lt;/code&gt; for everything and bounce later. Your probe says "valid." Reality disagrees a day after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You get blacklisted.&lt;/strong&gt; This is the one that ends the project. Opening thousands of SMTP connections that never deliver mail is behaviourally identical to a directory harvest attack. Providers rate-limit you, then tarpit you, then your IP lands on Spamhaus or Barracuda. If that IP is shared with your transactional email, you've just broken password resets for your entire user base in order to validate signups.&lt;/p&gt;

&lt;p&gt;You can work around this with a rotating pool of IPs, warmed reputation, per-domain rate limiting, connection reuse, and retry queues that respect greylisting. That is not a validation function. That's infrastructure with an on-call rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: catch-all domains, which nobody solves
&lt;/h2&gt;

&lt;p&gt;A catch-all domain accepts mail for every address at that domain and sorts it out internally. &lt;code&gt;anything@company.com&lt;/code&gt; returns &lt;code&gt;250&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no protocol-level way&lt;/strong&gt; to determine whether a specific mailbox exists behind a catch-all. Not slow, not expensive — impossible. The information isn't exposed.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds, because catch-all is common in exactly the segment you care about: B2B domains. Depending on the list, 15–25% of business domains behave this way.&lt;/p&gt;

&lt;p&gt;So every verification service in existence has to make a judgment call, and here's where they differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some return &lt;code&gt;valid&lt;/code&gt; and quietly inflate their accuracy numbers&lt;/li&gt;
&lt;li&gt;Some return &lt;code&gt;unknown&lt;/code&gt; and hand you the problem&lt;/li&gt;
&lt;li&gt;Some return a probabilistic score
I went with a score plus a &lt;strong&gt;reason string&lt;/strong&gt;, because "risky: 41" with no explanation is not actionable. If I can tell you the domain is catch-all, has a good sending history, and the local part matches a common pattern, you can make your own policy decision:
&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"catchAll"&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"catch_all_domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mx"&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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// your call, not mine&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;catchAll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;allowWithDoubleOptIn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest framing is that catch-all is a risk-management problem, not a validation problem. Anyone selling you certainty there is selling you something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: disposable domains rot faster than you think
&lt;/h2&gt;

&lt;p&gt;Blocking temp-mail is conceptually the easiest check: keep a list, compare against it.&lt;/p&gt;

&lt;p&gt;Then you look at how the list behaves. Disposable providers spin up new domains constantly — a single service can operate hundreds of rotating domains, and new ones appear daily. A static list you pulled from GitHub is meaningfully stale within weeks and badly stale within months.&lt;/p&gt;

&lt;p&gt;Keeping it current means monitoring the providers, watching newly-registered domain feeds, and looking at behavioural signals (domain age, MX pointing at known temp-mail infrastructure, patterns in the local part). It's a small, permanent, unglamorous job that never finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: the checks that aren't validity checks at all
&lt;/h2&gt;

&lt;p&gt;Two more things get lumped into "email verification" that are actually policy decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role-based addresses&lt;/strong&gt; — &lt;code&gt;info@&lt;/code&gt;, &lt;code&gt;admin@&lt;/code&gt;, &lt;code&gt;support@&lt;/code&gt;, &lt;code&gt;sales@&lt;/code&gt;. These are perfectly valid mailboxes. Whether you want them depends entirely on your product. A B2B tool probably does. A consumer app with per-seat billing probably doesn't. This is a flag, not a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free providers&lt;/strong&gt; — Gmail, Outlook, Proton. Also perfectly valid. Also might matter if you're gating a B2B free trial.&lt;/p&gt;

&lt;p&gt;The mistake is baking these into a boolean &lt;code&gt;valid&lt;/code&gt; field. They belong as separate flags so the caller can apply their own rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part specific to those of us in the EU
&lt;/h2&gt;

&lt;p&gt;An email address is personal data under GDPR. Sending it to a verification API means you're using a processor, and that carries paperwork regardless of where they are.&lt;/p&gt;

&lt;p&gt;If that processor is in the US, it's a Chapter V transfer, and you need a lawful basis for it. Post-&lt;em&gt;Schrems II&lt;/em&gt;, that's typically the processor's EU–US Data Privacy Framework certification, or Standard Contractual Clauses plus a transfer impact assessment. It is not illegal — it's genuinely fine when done properly — but it &lt;em&gt;is&lt;/em&gt; real work: you verify the certification is current, you document the transfer in your Article 30 records, you list them as a sub-processor, and you keep an eye on it, because the adequacy decision underpinning DPF has been challenged before and the previous two frameworks were both struck down.&lt;/p&gt;

&lt;p&gt;Most of the established players in this space are US-based. That's why I built Mailbeam EU-hosted with the DPA signed by default — not because US processors are inherently a problem, but because for an EU team the compliance overhead of "the data never leaves the EU" is a rounding error compared to the alternative, and I was tired of doing that assessment.&lt;/p&gt;

&lt;p&gt;If you're not in the EU, ignore all of this. If you are, it's about a day of work you can skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  So: build or buy?
&lt;/h2&gt;

&lt;p&gt;Genuinely build it yourself if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Syntax and MX checks are enough for your threat model — this is a completely reasonable place to land&lt;/li&gt;
&lt;li&gt;You're doing offline batch cleaning where latency doesn't matter and you can run a proper retry queue&lt;/li&gt;
&lt;li&gt;You have an IP reputation problem you're already solving for other reasons
Don't build it if you need real-time verification in a signup flow. The gap between "MX lookup" and "reliable at signup time" is IP reputation management, greylisting-aware retries, a live disposable feed, and catch-all scoring — four ongoing maintenance jobs, not four functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the actual build-vs-buy line, and it took me an embarrassingly long time to see it.&lt;/p&gt;




&lt;p&gt;If you want the version that already has all of this in it, &lt;a href="https://mailbeam.dev/" rel="noopener noreferrer"&gt;Mailbeam&lt;/a&gt; does the eight checks above in parallel in under 100ms, is hosted in Frankfurt, and prices per verification with no credit expiry. The free tier is 1,000/month with no card, which is enough for most side projects to just stay on forever.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mailbeam.dev/docs/quickstart" rel="noopener noreferrer"&gt;Docs and quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://mailbeam.dev/free-email-verifier" rel="noopener noreferrer"&gt;Free email verifier&lt;/a&gt; if you just want to check one address&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://mailbeam.dev/vs/zerobounce" rel="noopener noreferrer"&gt;How it compares to ZeroBounce&lt;/a&gt; if you're already using something
Happy to answer anything in the comments — particularly interested in how other people are handling catch-all domains, because I still don't think anyone has a great answer.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>api</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>AI Code Review: Making sure your code matches product requirements</title>
      <dc:creator>Alberto García</dc:creator>
      <pubDate>Sun, 24 Aug 2025 09:25:55 +0000</pubDate>
      <link>https://dev.to/alberto_garca_45e0d97494/ai-code-review-making-sure-your-code-matches-product-requirements-dn</link>
      <guid>https://dev.to/alberto_garca_45e0d97494/ai-code-review-making-sure-your-code-matches-product-requirements-dn</guid>
      <description>&lt;p&gt;If you’ve ever merged code that looked fine in review but later caused bugs, security issues, or didn’t even match the product requirements… you’re not alone.&lt;/p&gt;

&lt;p&gt;Traditional reviews focus on code quality. But shipping reliable software requires more: catching bugs, checking security, ensuring conventions, and verifying that what’s coded actually matches the product ticket.&lt;/p&gt;

&lt;p&gt;That’s why I built &lt;a href="https://heysopa.com/" rel="noopener noreferrer"&gt;Sopa&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Sopa does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Detects bugs &amp;amp; edge cases before they hit production&lt;/li&gt;
&lt;li&gt;Flags security vulnerabilities early&lt;/li&gt;
&lt;li&gt;Enforces style &amp;amp; conventions automatically&lt;/li&gt;
&lt;li&gt;Connects to Jira, Linear, ClickUp, Asana… to validate code against product requirements&lt;/li&gt;
&lt;li&gt;Provides suggestions &amp;amp; advice to fix issues fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sopa works with GitHub, GitLab and Bitbucket, reviewing pull requests or merge requests and leaving comments just like another reviewer on your team — but available 24/7.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prevent bugs before they reach QA or users&lt;/li&gt;
&lt;li&gt;Reduce the risk of security breaches&lt;/li&gt;
&lt;li&gt;Save developers hours of manual reviews and rework&lt;/li&gt;
&lt;li&gt;Align product and engineering seamlessly&lt;/li&gt;
&lt;li&gt;Ship faster with more confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Imagine a ticket says:&lt;br&gt;
“Add a search bar with autocomplete and log user queries for analytics.”&lt;/p&gt;

&lt;p&gt;A developer builds the search bar, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No autocomplete&lt;/li&gt;
&lt;li&gt;Analytics logging is missing&lt;/li&gt;
&lt;li&gt;And a small SQL injection risk is introduced in the query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sopa would:&lt;br&gt;
✅ Flag the missing autocomplete&lt;br&gt;
✅ Highlight the missing analytics logging (requirement not met)&lt;br&gt;
✅ Detect the potential SQL injection (security issue)&lt;br&gt;
✅ Suggest fixes to align with best practices&lt;/p&gt;

&lt;p&gt;All directly in the PR/MR comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;We just launched Sopa 🚀.&lt;br&gt;
I’d love to hear your feedback, ideas, or even your toughest “what if” scenarios.&lt;/p&gt;

&lt;p&gt;👉 Try it at &lt;a href="https://heysopa.com/" rel="noopener noreferrer"&gt;heysopa.com&lt;/a&gt; and let me know what you think.&lt;br&gt;
I’m building this with the community.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
