<?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>Forem: Tagg</title>
    <description>The latest articles on Forem by Tagg (@tagg_dev).</description>
    <link>https://forem.com/tagg_dev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3855664%2F79ef1341-90a0-494f-9111-7dcb903f3209.png</url>
      <title>Forem: Tagg</title>
      <link>https://forem.com/tagg_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tagg_dev"/>
    <language>en</language>
    <item>
      <title>Zero-Cost API Infrastructure: Running a DaaS Business on an Idle Server</title>
      <dc:creator>Tagg</dc:creator>
      <pubDate>Sun, 05 Apr 2026 08:17:51 +0000</pubDate>
      <link>https://forem.com/tagg_dev/zero-cost-api-infrastructure-running-a-daas-business-on-an-idle-server-fac</link>
      <guid>https://forem.com/tagg_dev/zero-cost-api-infrastructure-running-a-daas-business-on-an-idle-server-fac</guid>
      <description>&lt;h2&gt;
  
  
  I Had a Server Doing Nothing 22 Hours a Day
&lt;/h2&gt;

&lt;p&gt;I run quantitative trading bots — one for the Korean stock market, one for the US market, one for crypto. They sit on AWS and GCP, watching for signals, executing trades, then going back to sleep.&lt;/p&gt;

&lt;p&gt;Most of the time, these servers are idle. And AWS isn't free forever — once the 12-month free tier expired, I was looking at ~$10/month for a t2.micro doing almost nothing for most of the day.&lt;/p&gt;

&lt;p&gt;So I asked myself: &lt;strong&gt;what if the idle server could pay for itself?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea: Data-as-a-Service on RapidAPI
&lt;/h2&gt;

&lt;p&gt;I'm not a professional developer. My background is in IT architecture — I understand systems, infrastructure, and data flows. But I'd never built an API from scratch before.&lt;/p&gt;

&lt;p&gt;What changed was AI-assisted development. With vibe coding (writing in natural language, letting AI generate the code), I could suddenly build things that would have taken me months to learn the traditional way.&lt;/p&gt;

&lt;p&gt;I needed a product that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runs on the same server&lt;/strong&gt; as my trading bots without interference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requires minimal maintenance&lt;/strong&gt; — no daily babysitting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Has near-zero marginal cost&lt;/strong&gt; — data I can get for free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates recurring revenue&lt;/strong&gt; — subscriptions, not one-time sales&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pointed me to DaaS (Data-as-a-Service): take publicly available but hard-to-access data, clean it up, and serve it through a REST API on RapidAPI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Korean Data?
&lt;/h2&gt;

&lt;p&gt;Here's something most developers outside Korea don't realize: &lt;strong&gt;Korean government data is locked behind walls that have nothing to do with technology.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Government websites are Korean-only&lt;/li&gt;
&lt;li&gt;Authentication requires Korean phone numbers or certificates&lt;/li&gt;
&lt;li&gt;Data is scattered across multiple agencies in incompatible formats&lt;/li&gt;
&lt;li&gt;Documentation is in Korean PDFs, not API specs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a developer in Romania or Brazil trying to check whether a cosmetic ingredient is legal in Korea, you're stuck. Google Translate won't help you navigate a government portal that requires Korean authentication.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;moat&lt;/strong&gt;. Not a technical one — a linguistic and bureaucratic one. And it applies to dozens of Korean datasets that foreign businesses need.&lt;/p&gt;

&lt;p&gt;I chose cosmetic ingredients because K-Beauty is a global trend, and regulatory compliance data is something businesses will pay for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack: Deliberately Boring
&lt;/h2&gt;

&lt;p&gt;I intentionally picked the simplest tools that would work:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;Already using it for trading bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Fast, auto-docs, type validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;Zero config, read-only, fast for ~22K records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gunicorn + Uvicorn&lt;/td&gt;
&lt;td&gt;Production-ready, 2 workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Same environment everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AWS EC2 (existing)&lt;/td&gt;
&lt;td&gt;Already paying for it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Distribution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RapidAPI&lt;/td&gt;
&lt;td&gt;Handles auth, billing, marketing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Telegram alerts&lt;/td&gt;
&lt;td&gt;10 alert types, instant notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why SQLite?&lt;/strong&gt; My data is essentially static — updated monthly. SQLite in read-only mode gives me sub-50ms response times with zero database administration. No connection pools, no migration scripts, no separate database server. The entire database is a single file I can back up with &lt;code&gt;cp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why RapidAPI?&lt;/strong&gt; They take 20% of revenue, which stings. But they handle payment processing, API key management, rate limiting, and put my API in front of millions of developers. For a solo operation, that trade-off makes sense — especially at the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Structure: Actually Zero
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS EC2&lt;/td&gt;
&lt;td&gt;$0 (shared with trading bots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;$0 (not purchased yet)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL&lt;/td&gt;
&lt;td&gt;$0 (RapidAPI handles it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;$0 (SQLite file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;$0 (not needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;$0 (Telegram bot API is free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total fixed cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only cost is RapidAPI's 20% commission — but that only kicks in when I'm making money. Zero risk.&lt;/p&gt;

&lt;p&gt;If one PRO subscriber signs up at $29/month, I keep ~$23 after commission. That alone covers the AWS bill when free tier ends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resource Sharing: Bots + API on One Server
&lt;/h2&gt;

&lt;p&gt;This was the part I was most worried about. Trading bots need to execute quickly when signals fire. Would the API interfere?&lt;/p&gt;

&lt;p&gt;In practice, it's fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trading bots&lt;/strong&gt; spike CPU for a few seconds during market hours, then sleep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API&lt;/strong&gt; handles occasional requests throughout the day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; isolates the API from the bot processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite read-only&lt;/strong&gt; means no disk write contention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: most API businesses at the early stage get very few requests. I'm not serving 10,000 requests per second — I'm serving maybe 10 per day. The server barely notices.&lt;/p&gt;

&lt;p&gt;If traffic ever grows to the point where contention becomes real, that's a champagne problem. I'll spin up a dedicated instance and the API revenue will easily cover it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;K-Beauty Cosmetic Ingredients API&lt;/strong&gt; — 21,796 ingredients with regulatory data across 10 countries.&lt;/p&gt;

&lt;p&gt;Four pricing tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Requests/Month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BASIC&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRO&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULTRA&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEGA&lt;/td&gt;
&lt;td&gt;$199&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free tier exists for discovery. The paid tiers add more data fields, partial text search, and country-specific regulation data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infrastructure That Runs Itself
&lt;/h2&gt;

&lt;p&gt;Since I can't babysit the server, I automated everything I could:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telegram Alerts (10 types):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server start/stop&lt;/li&gt;
&lt;li&gt;500 errors (instant notification)&lt;/li&gt;
&lt;li&gt;Authentication failures&lt;/li&gt;
&lt;li&gt;Rate limit violations&lt;/li&gt;
&lt;li&gt;New subscriber / cancellation&lt;/li&gt;
&lt;li&gt;Daily stats and weekly revenue (planned)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Docker restart policy:&lt;/strong&gt;&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="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the container crashes at 3 AM, Docker brings it back up. I find out in the morning from the Telegram alert, but the API was down for maybe 5 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server management script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash deploy.sh
&lt;span class="c"&gt;# 1) Status check&lt;/span&gt;
&lt;span class="c"&gt;# 2) Restart&lt;/span&gt;
&lt;span class="c"&gt;# 3) Full redeploy (stop → build → run)&lt;/span&gt;
&lt;span class="c"&gt;# 4) Live logs&lt;/span&gt;
&lt;span class="c"&gt;# 5) Stop&lt;/span&gt;
&lt;span class="c"&gt;# 6) Rollback to previous DB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One script, six options. No remembering Docker commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data updates:&lt;/strong&gt; Monthly. Download two Excel files from the government open data portal, run a build script, deploy. Total time: about 15 minutes of hands-on work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security on a Budget
&lt;/h2&gt;

&lt;p&gt;Just because it's a side project doesn't mean I can skip security:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RapidAPI Proxy Secret&lt;/strong&gt; — blocks direct access to the server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-tier rate limiting&lt;/strong&gt; — prevents abuse (BASIC: 10/min, MEGA: 40/min)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation&lt;/strong&gt; — min/max length, type checking, SQL wildcard escaping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker non-root user&lt;/strong&gt; — container runs as &lt;code&gt;apiuser&lt;/code&gt;, not root&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite read-only mode&lt;/strong&gt; — even if someone gets in, they can't modify data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS headers&lt;/strong&gt; — forces HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security headers&lt;/strong&gt; — nosniff, frame deny, XSS protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cost of all this security: $0. It's all code-level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons After One Month
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The hardest part isn't building — it's marketing
&lt;/h3&gt;

&lt;p&gt;The API works. The docs are good. The data is solid. But discovery is the bottleneck. RapidAPI's marketplace helps, but it's not magic. I've written blog posts, set up GitHub examples, and optimized my listing. It's a slow grind.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Government data is messy but valuable
&lt;/h3&gt;

&lt;p&gt;Cleaning and normalizing data from multiple government sources took far longer than building the API. But that mess is exactly what creates value — if it were clean and easy to access, someone would have done it already.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Vibe coding works for real products
&lt;/h3&gt;

&lt;p&gt;I built this entire system — scraper, database builder, API server, deployment pipeline, monitoring — using AI-assisted development. Not as a toy project, but as a production service handling real requests. The key is having enough IT knowledge to architect the system, even if you can't write every line of code from memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Start with zero fixed costs
&lt;/h3&gt;

&lt;p&gt;If your side project costs $0 to run, you can wait indefinitely for product-market fit. No pressure to monetize immediately. No "burning runway." The server is already there, the tools are free, and RapidAPI only charges when you earn. This patience is a competitive advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revenue So Far
&lt;/h2&gt;

&lt;p&gt;Let's be honest: close to zero. One free-tier user from Romania tested the API. No paid subscribers yet.&lt;/p&gt;

&lt;p&gt;But that's okay. The API is live, the infrastructure runs itself, and my total investment is time — no money. I'll keep improving the product, writing about it, and waiting for the right customers to find it.&lt;/p&gt;

&lt;p&gt;The trading bots haven't made me rich either. But between DaaS subscriptions and quantitative trading, I'm building multiple income streams that don't require me to be physically present. That's the goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Framework: Applicable Beyond K-Beauty
&lt;/h2&gt;

&lt;p&gt;The approach works for any closed-ecosystem data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find data that's publicly available but practically inaccessible&lt;/strong&gt; (language barriers, authentication, format issues)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean it and serve it through a standard REST API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host it on infrastructure you're already paying for&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribute through a marketplace that handles billing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate monitoring so you don't have to watch it&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Korean business registration data, customs clearance codes, pharmaceutical approvals — there are dozens of datasets locked behind Korean-language government portals that international businesses need.&lt;/p&gt;

&lt;p&gt;Each one is a potential API product. Same stack, same server, same pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you're curious about the API itself:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://rapidapi.com/han8212/api/k-beauty-cosmetic-ingredients" rel="noopener noreferrer"&gt;K-Beauty Cosmetic Ingredients API on RapidAPI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📂 &lt;a href="https://github.com/han-tagg/Korean-cosmetic-ingredients-api" rel="noopener noreferrer"&gt;GitHub - Example Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're thinking about turning your own idle server into a side business, I'd love to hear your ideas. Drop a comment below.&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>api</category>
      <category>python</category>
      <category>indiehacker</category>
    </item>
    <item>
      <title>I Added Regulation Data From 10 Countries to My Cosmetic Ingredients API — Here's What I Found</title>
      <dc:creator>Tagg</dc:creator>
      <pubDate>Sat, 04 Apr 2026 15:08:49 +0000</pubDate>
      <link>https://forem.com/tagg_dev/i-added-regulation-data-from-10-countries-to-my-cosmetic-ingredients-api-heres-what-i-found-c73</link>
      <guid>https://forem.com/tagg_dev/i-added-regulation-data-from-10-countries-to-my-cosmetic-ingredients-api-heres-what-i-found-c73</guid>
      <description>&lt;h2&gt;
  
  
  The Backstory
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I launched a REST API for Korean cosmetic ingredients — 21,000+ ingredients from official Korean government sources, searchable by INCI name, CAS number, and Korean name.&lt;/p&gt;

&lt;p&gt;(&lt;a href="https://dev.to/tagg_dev/i-built-an-api-for-21000-korean-cosmetic-ingredients-heres-why-5bao"&gt;Original post here&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The API worked. But I kept getting the same question from the cosmetic industry people I talked to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Cool, but what about EU regulations? What about China?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair point. If you're formulating cosmetics for global markets, knowing that an ingredient is restricted &lt;em&gt;in Korea&lt;/em&gt; is only part of the puzzle. You need to know if it's also banned in the EU, restricted in China, or flagged in ASEAN.&lt;/p&gt;

&lt;p&gt;So I went down the rabbit hole.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Found: A Hidden Goldmine
&lt;/h2&gt;

&lt;p&gt;Korea's Ministry of Food and Drug Safety (MFDS) doesn't just track Korean regulations. Their database at &lt;a href="https://nedrug.mfds.go.kr" rel="noopener noreferrer"&gt;nedrug.mfds.go.kr&lt;/a&gt; contains &lt;strong&gt;regulation data for 10 countries&lt;/strong&gt;, all in one place:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Country&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EU&lt;/td&gt;
&lt;td&gt;5,301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASEAN&lt;/td&gt;
&lt;td&gt;4,843&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;China&lt;/td&gt;
&lt;td&gt;4,145&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;South Korea&lt;/td&gt;
&lt;td&gt;4,046&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brazil&lt;/td&gt;
&lt;td&gt;4,022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argentina&lt;/td&gt;
&lt;td&gt;4,022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taiwan&lt;/td&gt;
&lt;td&gt;2,137&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canada&lt;/td&gt;
&lt;td&gt;1,947&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japan&lt;/td&gt;
&lt;td&gt;386&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USA&lt;/td&gt;
&lt;td&gt;111&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;30,960 regulation records&lt;/strong&gt; total. Each one tells you whether an ingredient is prohibited or restricted in that country, with detailed conditions — concentration limits, product type restrictions, and usage warnings.&lt;/p&gt;

&lt;p&gt;The catch? The data is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedded in HTML pages as JavaScript JSON arrays&lt;/li&gt;
&lt;li&gt;Behind a Korean-language interface&lt;/li&gt;
&lt;li&gt;Spread across 7,257 individual pages&lt;/li&gt;
&lt;li&gt;No API, no download button&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scraping Challenge
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7,257 pages. One request at a time.
&lt;/h3&gt;

&lt;p&gt;The MFDS detail pages have an interesting structure. Each page contains a JavaScript variable called &lt;code&gt;arCountry&lt;/code&gt; — a JSON array with all the regulation data for that ingredient, across all countries. No AJAX calls needed. One page request = all countries.&lt;/p&gt;

&lt;p&gt;But there's a catch within the catch: some ingredients have &lt;strong&gt;both&lt;/strong&gt; restricted and prohibited data, stored in an if/else branch in the JavaScript. A naive regex extraction misses half the data. I had to write a bracket-depth counter to properly extract both arrays.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_json_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_pos&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Bracket counting instead of regex — 
       handles nested brackets in JSON strings&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;bracket_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bracket_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bracket_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small bug, big difference: one ingredient went from 0 regulation records to 5 after fixing this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying the Data
&lt;/h2&gt;

&lt;p&gt;Here's the thing about scraping government data from one country about &lt;em&gt;other&lt;/em&gt; countries: how do you know it's accurate?&lt;/p&gt;

&lt;p&gt;I cross-checked our EU data against the &lt;strong&gt;CosIng database&lt;/strong&gt; — the EU's official cosmetic ingredient database. CosIng publishes their Annex II (prohibited) and Annex III (restricted) lists as downloadable CSVs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification results:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total EU records from MFDS&lt;/td&gt;
&lt;td&gt;5,248&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matched against CosIng (by CAS number + name)&lt;/td&gt;
&lt;td&gt;4,693 (89.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regulation type accuracy&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type mismatches&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 38 mismatches weren't errors — they were edge cases where an ingredient is prohibited &lt;em&gt;when used as hair dye&lt;/em&gt; but restricted for other uses. Different classification logic, same underlying data.&lt;/p&gt;

&lt;p&gt;Good enough to ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New API (v3.0.0)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New endpoint: &lt;code&gt;/v1/ingredient/{code}/regulations&lt;/code&gt;
&lt;/h3&gt;



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

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://k-beauty-cosmetic-ingredients.p.rapidapi.com/v1/ingredient/9/regulations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EU&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k-beauty-cosmetic-ingredients.p.rapidapi.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&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;"ingredient"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"kr_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"inci_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Linalool"&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;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"available_countries"&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;"한국"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"regulate_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"notice_ingr_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1,6-Octadien-3-ol, 3,7-dimethyl-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"limit_condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"limit"&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;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;One API call. One ingredient. Regulations across multiple countries. No PDF digging, no Google Translate, no guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  Country access by tier
&lt;/h3&gt;

&lt;p&gt;Not everyone needs all 10 countries. So I tiered it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Countries&lt;/th&gt;
&lt;th&gt;Monthly Requests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BASIC&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Ingredients only (no regulations)&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRO&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;td&gt;South Korea, EU&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULTRA&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;td&gt;+ China, USA, Japan, ASEAN&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MEGA&lt;/td&gt;
&lt;td&gt;$199&lt;/td&gt;
&lt;td&gt;All 10 countries&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Interesting Findings From the Data
&lt;/h2&gt;

&lt;p&gt;After collecting all 30,960 regulation records, some patterns jumped out:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The EU bans the most ingredients
&lt;/h3&gt;

&lt;p&gt;EU leads with 5,301 regulation records. They're the strictest regulatory body for cosmetics — many other countries reference EU decisions when updating their own lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Prohibited" doesn't always mean "dangerous"
&lt;/h3&gt;

&lt;p&gt;Some ingredients are prohibited in cosmetics simply because they're classified as pharmaceuticals. Not because they're toxic — because they're too &lt;em&gt;effective&lt;/em&gt; and fall under drug regulation instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The same ingredient, different rules everywhere
&lt;/h3&gt;

&lt;p&gt;Take silver compounds: restricted in Canada (allowed in mouthwash up to 0.04%), but prohibited in the EU when in nano form. A global cosmetic brand needs to track these differences per-market.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Most MFDS regulated substances aren't in the KCIA ingredient dictionary
&lt;/h3&gt;

&lt;p&gt;Only 1,269 out of 7,257 MFDS regulated substances matched KCIA ingredients by CAS number. The rest are chemical substances that are &lt;em&gt;banned from&lt;/em&gt; cosmetics — they were never cosmetic ingredients to begin with.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why not add all MFDS substances to the main ingredients table?
&lt;/h3&gt;

&lt;p&gt;KCIA tracks what &lt;em&gt;can be&lt;/em&gt; used in cosmetics. MFDS tracks what &lt;em&gt;can't be&lt;/em&gt; (or has conditions). Mixing them would pollute the ingredient search results with non-cosmetic chemicals. Instead, I kept them in a separate &lt;code&gt;regulations&lt;/code&gt; table, linked by CAS number where possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why SQLite, still?
&lt;/h3&gt;

&lt;p&gt;Added 30,960 rows to the existing 21,796-ingredient database. SQLite handles it fine — the regulations table has indexes on &lt;code&gt;ingredient_code&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, and &lt;code&gt;regulate_type&lt;/code&gt;. Query time is still under 50ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rate limiting rabbit hole
&lt;/h3&gt;

&lt;p&gt;I wanted per-tier rate limits (BASIC: 10/min, MEGA: 40/min). Turns out the Python &lt;code&gt;slowapi&lt;/code&gt; library doesn't support dynamic rate limits based on request context. The decorator function gets called without access to the request object.&lt;/p&gt;

&lt;p&gt;Solution: two-layer approach. &lt;code&gt;slowapi&lt;/code&gt; handles the global ceiling (40/min), and a custom in-memory counter in the middleware enforces per-tier limits after the tier is detected from the subscription header.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated weekly updates&lt;/strong&gt; — KCIA change detection is already built, MFDS full re-scrape takes ~10 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API name/description SEO optimization&lt;/strong&gt; on RapidAPI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More search filters&lt;/strong&gt; for the regulations endpoint&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The API is live on RapidAPI with a free tier.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://rapidapi.com/han8212/api/k-beauty-cosmetic-ingredients" rel="noopener noreferrer"&gt;K-Beauty Cosmetic Ingredients API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📂 &lt;a href="https://github.com/han-tagg/Korean-cosmetic-ingredients-api" rel="noopener noreferrer"&gt;GitHub - Example Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building cosmetic tech, regulatory tools, or just curious about what's actually in your skincare products across different countries — give it a shot.&lt;/p&gt;

&lt;p&gt;Questions? Drop a comment below.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>data</category>
      <category>python</category>
    </item>
    <item>
      <title>15 Security Practices I Applied to My FastAPI Side Project</title>
      <dc:creator>Tagg</dc:creator>
      <pubDate>Fri, 03 Apr 2026 11:33:16 +0000</pubDate>
      <link>https://forem.com/tagg_dev/15-security-practices-i-applied-to-my-fastapi-side-project-301d</link>
      <guid>https://forem.com/tagg_dev/15-security-practices-i-applied-to-my-fastapi-side-project-301d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Disclaimer&lt;/strong&gt; &lt;br&gt;
These are practical measures I applied to my side project. &lt;br&gt;
For enterprise-grade security, consult professionals.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Post?
&lt;/h2&gt;

&lt;p&gt;I recently launched a &lt;a href="https://rapidapi.com/han8212/api/k-beauty-cosmetic-ingredients" rel="noopener noreferrer"&gt;K-Beauty Cosmetic Ingredients API&lt;/a&gt; on RapidAPI. Before going live, I spent significant time hardening security.&lt;/p&gt;

&lt;p&gt;This post shares &lt;strong&gt;15 practical security measures&lt;/strong&gt; I implemented, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Real code examples&lt;/li&gt;
&lt;li&gt;✅ OWASP Top 10 mapping&lt;/li&gt;
&lt;li&gt;✅ Lessons learned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you're deploying on RapidAPI, AWS, or anywhere else — these patterns apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Authentication &amp;amp; Access Control&lt;/li&gt;
&lt;li&gt;Server Hardening&lt;/li&gt;
&lt;li&gt;Database Security&lt;/li&gt;
&lt;li&gt;Monitoring &amp;amp; Alerting&lt;/li&gt;
&lt;li&gt;Resilience &amp;amp; Error Handling&lt;/li&gt;
&lt;li&gt;OWASP Top 10 Mapping&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Authentication &amp;amp; Access Control
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 RapidAPI Proxy Secret Validation
&lt;/h3&gt;

&lt;p&gt;RapidAPI acts as a proxy. But what stops someone from calling your server directly?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proxy Secret&lt;/strong&gt; — a shared secret that only RapidAPI knows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;RAPIDAPI_PROXY_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAPIDAPI_PROXY_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_rapidapi_proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Skip for health checks
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify secret
&lt;/span&gt;    &lt;span class="n"&gt;proxy_secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Proxy-Secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;proxy_secret&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;RAPIDAPI_PROXY_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A01: Broken Access Control&lt;/strong&gt; — Direct access blocked.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  1.2 Tier-Based Access Control
&lt;/h3&gt;

&lt;p&gt;Different subscription tiers get different features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_tier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tier_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Subscription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BASIC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tier_header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In endpoint:
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BASIC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Partial search requires PRO or ULTRA tier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... continue
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BASIC&lt;/td&gt;
&lt;td&gt;Exact search only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRO&lt;/td&gt;
&lt;td&gt;+ Partial search, more fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ULTRA&lt;/td&gt;
&lt;td&gt;+ All fields, highest limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  1.3 Rate Limiting with SlowAPI
&lt;/h3&gt;

&lt;p&gt;Prevent abuse and ensure fair usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Limiter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi.util&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_remote_address&lt;/span&gt;

&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_remote_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;limiter&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/v1/ingredient/inci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@limiter.limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;60/minute&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_by_inci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A07: Identification and Authentication Failures&lt;/strong&gt; — Brute force prevention.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Server Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Docker Non-Root User
&lt;/h3&gt;

&lt;p&gt;Never run containers as root:&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;# Create non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;groupadd &lt;span class="nt"&gt;-r&lt;/span&gt; apigroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; apigroup apiuser

&lt;span class="c"&gt;# Set ownership&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=apiuser:apigroup . /app&lt;/span&gt;

&lt;span class="c"&gt;# Switch to non-root user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; apiuser&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "main:app", "-w", "2", "-k", "uvicorn.workers.UvicornWorker"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A04: Insecure Design&lt;/strong&gt; — Principle of least privilege.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2.2 Sensitive File Permissions
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; files should only be readable by owner:&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="nb"&gt;chmod &lt;/span&gt;600 config/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-rw-------&lt;/span&gt;  1 ubuntu ubuntu  .env    &lt;span class="c"&gt;# Only owner can read/write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A05: Security Misconfiguration&lt;/strong&gt; — Sensitive data protected.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2.3 Environment Separation
&lt;/h3&gt;

&lt;p&gt;Different configs for different environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2.4 HSTS Header
&lt;/h3&gt;

&lt;p&gt;Force HTTPS connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_security_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&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;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max-age=31536000; includeSubDomains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A02: Cryptographic Failures&lt;/strong&gt; — Encrypted transport enforced.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Database Security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 OS-Level Read-Only Mode
&lt;/h3&gt;

&lt;p&gt;API only reads data. Why allow writes?&lt;/p&gt;

&lt;p&gt;Instead of relying on SQLite's &lt;code&gt;PRAGMA query_only&lt;/code&gt;, I used &lt;strong&gt;OS-level read-only mode&lt;/strong&gt; via URI parameter — a more robust approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# file: URI with ?mode=ro enforces read-only at OS level
&lt;/span&gt;    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DB_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?mode=ro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is stronger than &lt;code&gt;PRAGMA query_only&lt;/code&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OS-level enforcement&lt;/strong&gt; — even if SQLite has a bug, the OS blocks writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cannot be bypassed&lt;/strong&gt; — no way to disable via SQL commands&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A08: Software and Data Integrity Failures&lt;/strong&gt; — No unauthorized modifications.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3.2 Parameterized Queries
&lt;/h3&gt;

&lt;p&gt;Never concatenate user input into SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ BAD - SQL Injection vulnerable
&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM ingredients WHERE name = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ GOOD - Parameterized
&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM ingredients WHERE name = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A03: Injection&lt;/strong&gt; — SQL injection prevented.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3.3 LIKE Wildcard Escaping
&lt;/h3&gt;

&lt;p&gt;User input in LIKE queries can abuse wildcards (&lt;code&gt;%&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;escape_like_wildcards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Escape LIKE wildcards to prevent injection.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage
&lt;/span&gt;&lt;span class="n"&gt;escaped_q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;escape_like_wildcards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM ingredients WHERE inci_name LIKE ? ESCAPE &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\\\&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;escaped_q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&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;
  
  
  3.4 Query Timeout
&lt;/h3&gt;

&lt;p&gt;Prevent long-running queries from blocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DB_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?mode=ro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Monitoring &amp;amp; Alerting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Telegram Alerts on Server Start
&lt;/h3&gt;

&lt;p&gt;Know immediately when your server restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert_server_started&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
🚀 &amp;lt;b&amp;gt;API Server Started&amp;lt;/b&amp;gt;
├ Version: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
├ Environment: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
├ DB Records: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;db_count&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
└ Time: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;send_telegram_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4.2 Real-Time Error Alerts
&lt;/h3&gt;

&lt;p&gt;500 errors go straight to Telegram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.exception_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;global_exception_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;alert_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;error_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal server error&lt;/span&gt;&lt;span class="sh"&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;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;OWASP A09: Security Logging and Monitoring Failures&lt;/strong&gt; — Real-time visibility.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4.3 Preventing Duplicate Alerts
&lt;/h3&gt;

&lt;p&gt;Multiple Gunicorn workers = multiple startup alerts? Not anymore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lock_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOG_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.startup_alert_lock&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# Clean up stale lock files (server restart case)
&lt;/span&gt;    &lt;span class="c1"&gt;# Race Condition defense: ignore if another worker deleted it first
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;file_age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getmtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file_age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Another worker deleted it first — continue
&lt;/span&gt;
    &lt;span class="c1"&gt;# Atomic file creation - only first worker succeeds
&lt;/span&gt;    &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_CREAT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_EXCL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_WRONLY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;alert_server_started&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# First worker sends
&lt;/span&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;FileExistsError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Other workers skip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TOCTOU (Time-of-check to time-of-use)&lt;/strong&gt; — Atomic &lt;code&gt;O_EXCL&lt;/code&gt; operation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Race conditions&lt;/strong&gt; — &lt;code&gt;OSError&lt;/code&gt; catch for concurrent access&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4.4 Structured JSON Logging
&lt;/h3&gt;

&lt;p&gt;Logs that are easy to parse and analyze:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_api_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_time_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;log_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_time_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response_time_ms&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_entry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Resilience &amp;amp; Error Handling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Graceful Logging Failures
&lt;/h3&gt;

&lt;p&gt;Log failures shouldn't crash your API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;safe_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;log_api_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Log failure doesn't affect API response
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.2 Telegram Retry Logic
&lt;/h3&gt;

&lt;p&gt;Network is unreliable. Retry with backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib3.util.retry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Retry&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.adapters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPAdapter&lt;/span&gt;

&lt;span class="n"&gt;retry_strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;backoff_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# 0.5s, 1s, 2s
&lt;/span&gt;    &lt;span class="n"&gt;status_forcelist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;respect_retry_after_header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# Honor 429 Retry-After
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HTTPAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retry_strategy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.3 Input Sanitization for Alerts
&lt;/h3&gt;

&lt;p&gt;User input in Telegram messages? Escape it:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;alert_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;safe_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error_message&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Telegram limit
&lt;/span&gt;    &lt;span class="nf"&gt;send_telegram_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;safe_message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.4 Docker Auto-Restart
&lt;/h3&gt;

&lt;p&gt;Container crashes? Auto-recover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; myapi &lt;span class="se"&gt;\&lt;/span&gt;
  myapi:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5.5 Health Check Endpoint
&lt;/h3&gt;

&lt;p&gt;For load balancers and monitoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health_check&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.3.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&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:8000/health || exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6. OWASP Top 10 Mapping
&lt;/h2&gt;

&lt;p&gt;Here's how our security measures map to OWASP Top 10 (2021):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OWASP&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Our Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A01&lt;/td&gt;
&lt;td&gt;Broken Access Control&lt;/td&gt;
&lt;td&gt;✅ Proxy Secret, Tier-based access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A02&lt;/td&gt;
&lt;td&gt;Cryptographic Failures&lt;/td&gt;
&lt;td&gt;✅ HSTS header, HTTPS enforced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A03&lt;/td&gt;
&lt;td&gt;Injection&lt;/td&gt;
&lt;td&gt;✅ Parameterized SQL, LIKE escaping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A04&lt;/td&gt;
&lt;td&gt;Insecure Design&lt;/td&gt;
&lt;td&gt;✅ Non-root Docker, read-only DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A05&lt;/td&gt;
&lt;td&gt;Security Misconfiguration&lt;/td&gt;
&lt;td&gt;✅ .env permissions, env separation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A06&lt;/td&gt;
&lt;td&gt;Vulnerable Components&lt;/td&gt;
&lt;td&gt;⚠️ Regular dependency updates needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A07&lt;/td&gt;
&lt;td&gt;Auth Failures&lt;/td&gt;
&lt;td&gt;✅ Rate limiting, secret validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A08&lt;/td&gt;
&lt;td&gt;Data Integrity&lt;/td&gt;
&lt;td&gt;✅ OS-level read-only database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A09&lt;/td&gt;
&lt;td&gt;Logging Failures&lt;/td&gt;
&lt;td&gt;✅ JSON logging, Telegram alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A10&lt;/td&gt;
&lt;td&gt;SSRF&lt;/td&gt;
&lt;td&gt;➖ N/A (no external requests)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Coverage: 9/10 categories addressed&lt;/strong&gt; ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Security is Layers
&lt;/h3&gt;

&lt;p&gt;No single measure is enough. Defense in depth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network → Proxy Secret&lt;/li&gt;
&lt;li&gt;Application → Rate limiting, input validation&lt;/li&gt;
&lt;li&gt;Data → Read-only, parameterized queries&lt;/li&gt;
&lt;li&gt;Infrastructure → Non-root, auto-restart&lt;/li&gt;
&lt;li&gt;Monitoring → Real-time alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Fail Gracefully
&lt;/h3&gt;

&lt;p&gt;Security logging shouldn't break your API. Wrap everything in try-except.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Assume Breach
&lt;/h3&gt;

&lt;p&gt;Design as if attackers will get in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only database (can't modify)&lt;/li&gt;
&lt;li&gt;Non-root container (limited damage)&lt;/li&gt;
&lt;li&gt;Monitoring (you'll know quickly)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Automate Everything
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Auto-restart on crash&lt;/li&gt;
&lt;li&gt;Auto-retry on network failure&lt;/li&gt;
&lt;li&gt;Auto-alert on errors&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Test Your Security
&lt;/h3&gt;

&lt;p&gt;Before launch:&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;# Test direct access (should fail)&lt;/span&gt;
curl http://your-server:8000/v1/stats
&lt;span class="c"&gt;# Expected: 403 Forbidden&lt;/span&gt;

&lt;span class="c"&gt;# Test with valid secret (should work)&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Proxy-Secret: your-secret"&lt;/span&gt; http://your-server:8000/v1/stats
&lt;span class="c"&gt;# Expected: 200 OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Security isn't a feature — it's a foundation. These 15 measures took extra time upfront, but give me confidence that the API can handle production traffic safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/Top10/" rel="noopener noreferrer"&gt;OWASP Top 10 (2021)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fastapi.tiangolo.com/tutorial/security/" rel="noopener noreferrer"&gt;FastAPI Security Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/develop/security-best-practices/" rel="noopener noreferrer"&gt;Docker Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try the API
&lt;/h2&gt;

&lt;p&gt;If you're curious about the API itself:&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://rapidapi.com/han8212/api/k-beauty-cosmetic-ingredients" rel="noopener noreferrer"&gt;K-Beauty Cosmetic Ingredients API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📂 &lt;a href="https://github.com/han-tagg/Korean-cosmetic-ingredients-api" rel="noopener noreferrer"&gt;GitHub - Example Code&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions about any of these security measures? Drop a comment below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built an API for 21,000+ Korean Cosmetic Ingredients — Here's Why</title>
      <dc:creator>Tagg</dc:creator>
      <pubDate>Wed, 01 Apr 2026 13:32:33 +0000</pubDate>
      <link>https://forem.com/tagg_dev/i-built-an-api-for-21000-korean-cosmetic-ingredients-heres-why-5bao</link>
      <guid>https://forem.com/tagg_dev/i-built-an-api-for-21000-korean-cosmetic-ingredients-heres-why-5bao</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to check whether a cosmetic ingredient is legal in South Korea, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🇰🇷 Government websites only in Korean&lt;/li&gt;
&lt;li&gt;📄 Data buried in PDFs, not databases&lt;/li&gt;
&lt;li&gt;🔍 No search functionality&lt;/li&gt;
&lt;li&gt;🚫 No official API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For cosmetic brands trying to enter the Korean market, this means hours of manual research, expensive consultants, or just... guessing.&lt;/p&gt;

&lt;p&gt;I decided to fix this.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;K-Beauty Cosmetic Ingredients API&lt;/strong&gt; — A REST API with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;21,796 ingredients&lt;/strong&gt; from official Korean sources&lt;/li&gt;
&lt;li&gt;✅ INCI names, CAS numbers, Korean translations&lt;/li&gt;
&lt;li&gt;✅ Regulatory status (Prohibited / Restricted / Not Listed)&lt;/li&gt;
&lt;li&gt;✅ Concentration limits and conditions&lt;/li&gt;
&lt;li&gt;✅ Fast JSON responses (&amp;lt;50ms)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data comes directly from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MFDS&lt;/strong&gt; (Ministry of Food and Drug Safety) — Korea's FDA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KCIA&lt;/strong&gt; (Korea Cosmetic Industry Association)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Want to check if Retinol is restricted in Korea?&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://k-beauty-cosmetic-ingredients.p.rapidapi.com/v1/ingredient/inci&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retinol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;k-beauty-cosmetic-ingredients.p.rapidapi.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&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;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inci_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RETINOL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kr_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"cas_numbers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"68-26-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"regulation_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Restricted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"purposes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Skin conditioning"&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;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;Now you know: &lt;strong&gt;Retinol is restricted&lt;/strong&gt; (allowed with conditions) in Korean cosmetics. No PDF digging required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Is This For?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🧪 &lt;strong&gt;Cosmetic Formulators&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Check ingredients before formulating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📋 &lt;strong&gt;Regulatory Consultants&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Speed up compliance audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🏭 &lt;strong&gt;Manufacturers&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Validate formulations for Korea export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;📱 &lt;strong&gt;App Developers&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Build ingredient scanner apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛒 &lt;strong&gt;E-commerce&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Show safety info on product pages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  API Endpoints
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/ingredient/inci?q=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search by INCI name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/ingredient/cas?q=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search by CAS number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/ingredient/kr?q=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search by Korean name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/ingredient/search?q=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial text search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/ingredient/status?s=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Filter by regulatory status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/v1/stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Database statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;For those curious about how it's built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Python, FastAPI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; SQLite (read-only mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; Gunicorn + Uvicorn workers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; AWS EC2, Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; Telegram alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution:&lt;/strong&gt; RapidAPI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; OWASP Top 10 hardened (Rate Limiting, Input Validation, LIKE Wildcard Escaping, Proxy Secret)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why SQLite?
&lt;/h3&gt;

&lt;p&gt;The data is essentially static (updated monthly). SQLite in read-only mode gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero configuration&lt;/li&gt;
&lt;li&gt;Fast reads&lt;/li&gt;
&lt;li&gt;No separate database server&lt;/li&gt;
&lt;li&gt;Easy backups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a read-heavy API with ~22K records, it's perfect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Government Data is Messy
&lt;/h3&gt;

&lt;p&gt;Scraping and normalizing data from multiple sources took 10x longer than building the API itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. B2B &amp;gt; B2C for Niche APIs
&lt;/h3&gt;

&lt;p&gt;Consumer apps want "safety scores." Businesses want &lt;strong&gt;regulatory compliance data&lt;/strong&gt;. They're willing to pay for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Documentation Matters
&lt;/h3&gt;

&lt;p&gt;Good docs = fewer support questions = more time for features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Free
&lt;/h2&gt;

&lt;p&gt;The API is live on RapidAPI with a free tier (100 requests/month).&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://rapidapi.com/han8212/api/k-beauty-cosmetic-ingredients" rel="noopener noreferrer"&gt;K-Beauty Cosmetic Ingredients API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;📂 &lt;strong&gt;&lt;a href="https://github.com/han-tagg/Korean-cosmetic-ingredients-api" rel="noopener noreferrer"&gt;GitHub - Example Code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Monthly data updates&lt;/li&gt;
&lt;li&gt;[ ] More search filters&lt;/li&gt;
&lt;li&gt;[ ] Batch lookup endpoint&lt;/li&gt;
&lt;li&gt;[ ] Python/JS SDK packages&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Questions?
&lt;/h2&gt;

&lt;p&gt;Drop a comment below or reach out on RapidAPI. Happy to help!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're working on cosmetic tech or regulatory compliance tools, I'd love to hear about your use case.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
