<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Robert Tidball</title>
    <description>The latest articles on DEV Community by Robert Tidball (@roberttidball).</description>
    <link>https://dev.to/roberttidball</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608997%2F531d13ca-ef6b-4bba-8ee2-56cac876c320.jpeg</url>
      <title>DEV Community: Robert Tidball</title>
      <link>https://dev.to/roberttidball</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roberttidball"/>
    <language>en</language>
    <item>
      <title>Your MCP Server Doesn't Need 40 Tools</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Mon, 22 Jun 2026 13:48:03 +0000</pubDate>
      <link>https://dev.to/roberttidball/your-mcp-server-doesnt-need-40-tools-2ig1</link>
      <guid>https://dev.to/roberttidball/your-mcp-server-doesnt-need-40-tools-2ig1</guid>
      <description>&lt;p&gt;MCP demos make it look like the win is exposing everything.&lt;/p&gt;

&lt;p&gt;"Here are all my endpoints. Here are all my database tables. Here are all my internal actions. The agent can call anything now."&lt;/p&gt;

&lt;p&gt;That feels powerful for about ten minutes.&lt;/p&gt;

&lt;p&gt;Then the model calls the wrong tool, passes the right argument in the wrong shape, asks for a chart from a search endpoint, retries a destructive action, or returns an answer that sounds confident because the tool name was vague enough to mean three different things.&lt;/p&gt;

&lt;p&gt;The problem is not MCP. The problem is treating MCP like a magic adapter for your backend.&lt;/p&gt;

&lt;p&gt;An MCP server is not just "my API, but agent-accessible." It is a product surface for a very literal, very distractible user.&lt;/p&gt;

&lt;p&gt;That user happens to be a language model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: one endpoint, one tool
&lt;/h2&gt;

&lt;p&gt;The first instinct is obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users/:id        -&amp;gt; get_user
GET /users            -&amp;gt; list_users
GET /invoices/:id     -&amp;gt; get_invoice
GET /invoices         -&amp;gt; list_invoices
GET /events           -&amp;gt; list_events
POST /events/search   -&amp;gt; search_events
POST /reports         -&amp;gt; create_report
GET /reports/:id      -&amp;gt; get_report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks clean because it mirrors the API.&lt;/p&gt;

&lt;p&gt;It is often bad for agents.&lt;/p&gt;

&lt;p&gt;Humans can read docs, understand product context, and choose between similar routes. Models do not really "understand" your product. They pattern-match over names, descriptions, schemas, previous messages, and whatever context still fits in the window.&lt;/p&gt;

&lt;p&gt;If you give the model 40 similar tools, you did not give it power. You gave it 40 ways to be almost right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in production
&lt;/h2&gt;

&lt;p&gt;The lesson became obvious while building the MCP layer for FXMacroData.&lt;/p&gt;

&lt;p&gt;The backend has normal API routes for calendar events, FX pairs, rates, COT positioning, commodities, bond yields, metadata, and docs. Mirroring every route into MCP would have looked comprehensive, but it would have made the agent choose between too many near-matches.&lt;/p&gt;

&lt;p&gt;The useful MCP boundary was smaller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one tool for the market summary;&lt;/li&gt;
&lt;li&gt;one for the release calendar;&lt;/li&gt;
&lt;li&gt;one for an FX pair snapshot;&lt;/li&gt;
&lt;li&gt;one for chartable indicator history;&lt;/li&gt;
&lt;li&gt;one for COT positioning;&lt;/li&gt;
&lt;li&gt;one for commodities;&lt;/li&gt;
&lt;li&gt;one for bond yields;&lt;/li&gt;
&lt;li&gt;one for data coverage and metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is less impressive in a demo.&lt;/p&gt;

&lt;p&gt;It is more useful in a real chat.&lt;/p&gt;

&lt;p&gt;The agent no longer needs to know whether a question maps to an endpoint, a cached dashboard payload, or a normalized data table. It only needs to pick the user intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool is a promise
&lt;/h2&gt;

&lt;p&gt;An API route says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you send this request, the server will respond.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An MCP tool should say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use me for this exact job, with these exact inputs, and expect this kind of result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means a good tool description is not marketing copy. It is routing logic for the model.&lt;/p&gt;

&lt;p&gt;Bad:&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;"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;"get_data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Gets data from the API."&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;Better:&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;"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;"lookup_release_calendar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Return scheduled economic release events for one currency and date range. Use this before answering questions about upcoming macro events."&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;Even if you do not care about finance, the pattern matters. The second tool tells the model when to use it, what it returns, and what kind of user question it supports.&lt;/p&gt;

&lt;p&gt;That is the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fewer tools, sharper edges
&lt;/h2&gt;

&lt;p&gt;I would rather give an agent 8 boring tools than 45 clever ones.&lt;/p&gt;

&lt;p&gt;The boring tools should map to user intent, not to your internal route structure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;User intent&lt;/th&gt;
&lt;th&gt;Better tool shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"What is coming up?"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup_calendar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What happened recently?"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lookup_recent_events&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Show me the chartable history"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_time_series&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Explain this result"&lt;/td&gt;
&lt;td&gt;not a tool; let the model write from tool output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Export this"&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;create_export&lt;/code&gt; only if exports are a real product action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important part is that the model does not need to assemble your backend architecture in its head before making a useful call.&lt;/p&gt;

&lt;p&gt;That is your job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdn2wjk2y5zfvp4kdw9ag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdn2wjk2y5zfvp4kdw9ag.png" alt="Diagram comparing endpoint-shaped MCP tools with a smaller intent-shaped MCP tool layer" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Names matter more than you want them to
&lt;/h2&gt;

&lt;p&gt;Developers love compact names.&lt;/p&gt;

&lt;p&gt;Models need boring names.&lt;/p&gt;

&lt;p&gt;I used to like names like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query
fetch
resolve
search
get_context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those names are convenient for us and mushy for a model. They force the model to infer too much from the description.&lt;/p&gt;

&lt;p&gt;Prefer names that carry product intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_docs
lookup_account_status
get_time_series
list_recent_errors
create_support_summary
check_deployment_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Longer is fine. Ambiguous is expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not return your whole database row
&lt;/h2&gt;

&lt;p&gt;The response shape matters as much as the input schema.&lt;/p&gt;

&lt;p&gt;If a tool returns a giant nested object, the model will happily use the wrong field. It may cite an internal note, confuse &lt;code&gt;created_at&lt;/code&gt; with &lt;code&gt;updated_at&lt;/code&gt;, or summarize an implementation detail that was never meant for users.&lt;/p&gt;

&lt;p&gt;Return the smallest shape that supports the job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowhcqik9yfilidq0b7y2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fowhcqik9yfilidq0b7y2.png" alt="Diagram showing an oversized database-row response becoming a smaller MCP response contract with status, items, and next_action" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Release calendar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/calendar"&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;"next_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Use these rows to answer the user's calendar question."&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;That &lt;code&gt;next_action&lt;/code&gt; field looks silly until you watch models behave better with it.&lt;/p&gt;

&lt;p&gt;You are not only returning data. You are returning affordances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build for failure, not the happy path
&lt;/h2&gt;

&lt;p&gt;Most tool demos show the successful call.&lt;/p&gt;

&lt;p&gt;Production quality comes from boring failure states.&lt;/p&gt;

&lt;p&gt;What should the tool return when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no records match;&lt;/li&gt;
&lt;li&gt;the user asked for something outside their permissions;&lt;/li&gt;
&lt;li&gt;the input is valid but too broad;&lt;/li&gt;
&lt;li&gt;the upstream data is stale;&lt;/li&gt;
&lt;li&gt;the operation is risky and needs confirmation;&lt;/li&gt;
&lt;li&gt;the model picked the wrong tool?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not make the model infer all of that from a 500 or an empty array.&lt;/p&gt;

&lt;p&gt;For example:&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;"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;"no_results"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No matching rows were found for that date range."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suggested_next_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ask the user whether they want to widen the date range."&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;This is not just nicer UX. It reduces hallucination pressure. The model has something accurate to say instead of filling the silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your OpenAPI schema still matters
&lt;/h2&gt;

&lt;p&gt;MCP does not replace API design.&lt;/p&gt;

&lt;p&gt;If anything, it makes sloppy API design more visible.&lt;/p&gt;

&lt;p&gt;A clean OpenAPI schema gives you a source of truth for types, descriptions, auth requirements, and examples. An MCP layer can then expose a smaller set of agent-friendly tools on top.&lt;/p&gt;

&lt;p&gt;The stack I like looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend API
  -&amp;gt; documented OpenAPI schema
  -&amp;gt; small MCP tool layer
  -&amp;gt; agent instructions and examples
  -&amp;gt; smoke tests that call tools like a model would
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP server should not become a second undocumented API. That just creates two surfaces to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the tool list like a UI
&lt;/h2&gt;

&lt;p&gt;If a button says "Delete draft", you review the label.&lt;/p&gt;

&lt;p&gt;If an MCP tool says &lt;code&gt;run_action&lt;/code&gt;, you should review that label too.&lt;/p&gt;

&lt;p&gt;I like a simple test that dumps the tool list and asks human questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can I tell when each tool should be used?&lt;/li&gt;
&lt;li&gt;Do two tools sound like they do the same thing?&lt;/li&gt;
&lt;li&gt;Is any tool name too generic?&lt;/li&gt;
&lt;li&gt;Are destructive tools clearly marked?&lt;/li&gt;
&lt;li&gt;Are required arguments obvious?&lt;/li&gt;
&lt;li&gt;Does each tool return a shape the model can summarize safely?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run actual tool calls with boring prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What changed this week?
Show me recent errors.
Find docs about API keys.
Create a short summary for a new user.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model keeps choosing the wrong tool, do not just "improve the prompt." Fix the tool boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;I have been thinking about this while working on the public API, OpenAPI, docs, and MCP surfaces around &lt;a href="https://fxmacrodata.com/" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The domain is not the point. The lesson is.&lt;/p&gt;

&lt;p&gt;Once you expose product data to agents, your API is no longer only for deterministic callers. It is also for a model that needs strong names, tight schemas, clear failures, and fewer choices than a human developer.&lt;/p&gt;

&lt;p&gt;The better the boundary, the less "agentic" magic you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I use now
&lt;/h2&gt;

&lt;p&gt;Before adding a new MCP tool, I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a real user intent, or just an endpoint I happen to have?&lt;/li&gt;
&lt;li&gt;Could an existing tool answer the same question?&lt;/li&gt;
&lt;li&gt;Is the name boring and specific?&lt;/li&gt;
&lt;li&gt;Does the description say when to use it?&lt;/li&gt;
&lt;li&gt;Are the arguments narrow enough?&lt;/li&gt;
&lt;li&gt;Is the response shape smaller than the internal object?&lt;/li&gt;
&lt;li&gt;Does it handle no-results and permission failures clearly?&lt;/li&gt;
&lt;li&gt;Would I be comfortable with the model quoting the response directly?&lt;/li&gt;
&lt;li&gt;Do I have at least one smoke prompt that should call this tool?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is fuzzy, I probably do not need a new tool yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;MCP is useful because it gives agents a standard way to use external systems.&lt;/p&gt;

&lt;p&gt;But standards do not save bad boundaries.&lt;/p&gt;

&lt;p&gt;Your MCP server does not need to expose everything your backend can do. It needs to expose the few things an agent should do, with names and schemas that make the right call obvious.&lt;/p&gt;

&lt;p&gt;The best agent tool is not the most powerful one.&lt;/p&gt;

&lt;p&gt;It is the one the model cannot easily misunderstand.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Introducing the FXMacroData Economic Data Embed Widget</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Wed, 03 Dec 2025 13:34:36 +0000</pubDate>
      <link>https://dev.to/fxmacrodata/introducing-the-fxmacrodata-economic-data-embed-widget-3oin</link>
      <guid>https://dev.to/fxmacrodata/introducing-the-fxmacrodata-economic-data-embed-widget-3oin</guid>
      <description>&lt;p&gt;Today, we’re launching a major new feature on &lt;a href="https://fxmacrodata.com/" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt; — fully embeddable, real-time charts that you can drop into any website, blog, or research portal with a single copy-and-paste.&lt;/p&gt;

&lt;p&gt;Each chart on FXMacroData now includes a dedicated “Embed Graph” button. Clicking it reveals a ready-to-paste HTML snippet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdo0aheb2g7tr6f56x1uk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdo0aheb2g7tr6f56x1uk.png" alt="Graph with embed option" width="439" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can view the full data and chart on the dashboard here: &lt;a href="https://fxmacrodata.com/dashboard/EUR_USD" rel="noopener noreferrer"&gt;https://fxmacrodata.com/dashboard/EUR_USD&lt;/a&gt; To embed the chart above on your own site, use this snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- FXMacroData Chart Embed --&amp;gt;
&amp;lt;div
    id="fxmacro-carryChart"
    style="width: 100%; max-width: 800px; margin: 20px auto;"
&amp;gt;
    &amp;lt;iframe
        src="https://fxmacrodata.com/dashboard/embed/EUR_USD/carryChart"
        width="100%"
        height="450"
        frameborder="0"
        style="border: 1px solid #e5e7eb; border-radius: 8px;"
    &amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Embeds Work
&lt;/h2&gt;

&lt;p&gt;Embedding is as simple as copying the snippet and pasting it into your page. Each FXMacroData embed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates in real time as new announcements are released&lt;/li&gt;
&lt;li&gt;Requires zero maintenance or API integration&lt;/li&gt;
&lt;li&gt;Loads quickly thanks to a lightweight hosting layer&lt;/li&gt;
&lt;li&gt;Automatically adapts to your layout and device size&lt;/li&gt;
&lt;li&gt;Built for Analysts, Traders, and Educators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you’re building models, writing research, teaching, or running a fintech product, these embeds let you present live macroeconomic data without engineering work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designed for Reliability and Performance
&lt;/h2&gt;

&lt;p&gt;Each widget runs on the same architecture powering the FXMacroData dashboards, backed by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time streaming data from our announcement pipeline&lt;/li&gt;
&lt;li&gt;Optimized caching for low latency&lt;/li&gt;
&lt;li&gt;Cloud Run auto-scaling for high-volatility events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your embeds stay fast, accurate, and stable — even during major releases when traffic spikes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Embedding Today
&lt;/h2&gt;

&lt;p&gt;Visit any dashboard on FXMacroData and click “Embed Graph” to generate your snippet instantly.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Creating a python SDK for FXMacroData</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Tue, 25 Nov 2025 13:46:19 +0000</pubDate>
      <link>https://dev.to/roberttidball/-2g4b</link>
      <guid>https://dev.to/roberttidball/-2g4b</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/fxmacrodata" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11901%2Fbe16b3bd-be49-43fe-9789-919ea63acc35.png" alt="FXMacroData" width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608997%2F531d13ca-ef6b-4bba-8ee2-56cac876c320.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API&lt;/h2&gt;
      &lt;h3&gt;Robert Tidball for FXMacroData ・ Nov 25&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#python&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#api&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>python</category>
      <category>api</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Tue, 25 Nov 2025 13:45:20 +0000</pubDate>
      <link>https://dev.to/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc</link>
      <guid>https://dev.to/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc</guid>
      <description>&lt;p&gt;When building a Python library, the goal is to turn a complex, boilerplate-heavy process (raw API calls) into a simple, elegant one-liner. The &lt;strong&gt;&lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fxmacrodata" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt;&lt;/strong&gt; API provides real-time macroeconomic indicators for major currency pairs—a goldmine for quant traders and analysts.&lt;/p&gt;

&lt;p&gt;Raw API calls force developers to repeat code for authentication, error checking, and URL construction. Embracing the &lt;strong&gt;DRY (Don't Repeat Yourself)&lt;/strong&gt; principle, I set out to build a dedicated Python library on top of it, creating a user-friendly wrapper. This article walks you through the core components of that wrapper, covering synchronous and asynchronous clients, proper exception handling, and utility functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Project Scaffolding and Handling Authentication
&lt;/h2&gt;

&lt;p&gt;A good library starts with an intuitive entry point. My goal was to turn an HTTP request into a clean Python method call like:&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;client&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;aud&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;inflation&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;
  
  
  The Client Constructor
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Client&lt;/code&gt; class holds the base URL and API key. The FXMacroData API has a unique feature: &lt;strong&gt;USD data is public, but other currencies require an API key.&lt;/strong&gt; The constructor handles this requirement upfront.&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;# client.py or async_client.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FXMacroDataError&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://fxmacrodata.com/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Synchronous FXMacroData Client.
        api_key: Required for non-USD currencies. USD is public.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Core Logic: The Synchronous Client (&lt;code&gt;Client&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;synchronous &lt;code&gt;Client&lt;/code&gt;&lt;/strong&gt; uses the popular &lt;code&gt;requests&lt;/code&gt; library. The main logic resides in the &lt;code&gt;get&lt;/code&gt; method, which dynamically constructs the URL and enforces the API key requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;get&lt;/code&gt; Method: Dynamic URL Construction and Key Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# client.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&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;indicator&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;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&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;currency&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;indicator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Custom exception is crucial for user-friendly errors
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&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;API key required for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; endpoints.&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&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="c1"&gt;# ... params and API call logic ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Robust Error Handling with Custom Exceptions
&lt;/h3&gt;

&lt;p&gt;A robust library must handle failures gracefully. I created a custom exception, &lt;code&gt;FXMacroDataError&lt;/code&gt;, to catch network issues and non-200 status codes, returning a clear, actionable message.&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;# exceptions.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom exception for FXMacroData client errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Core request logic with the error wrapper:&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;# client.py (continued)
&lt;/span&gt;&lt;span class="k"&gt;try&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="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="n"&gt;url&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="n"&gt;headers&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="n"&gt;params&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;FXMacroDataError&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;Request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="k"&gt;if&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;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Raise a clear error if the API returns a problem
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&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;API Error (&lt;/span&gt;&lt;span class="si"&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;status_code&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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="k"&gt;return&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;h2&gt;
  
  
  3. Advanced Feature: The Asynchronous Client (&lt;code&gt;AsyncClient&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;For automated trading bots or high-traffic dashboards, &lt;strong&gt;asynchronous programming&lt;/strong&gt; is essential for performance. The &lt;code&gt;AsyncClient&lt;/code&gt; uses the &lt;strong&gt;&lt;code&gt;aiohttp&lt;/code&gt;&lt;/strong&gt; library for non-blocking I/O.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asynchronous Session Management
&lt;/h3&gt;

&lt;p&gt;I implemented async context managers (&lt;code&gt;__aenter__&lt;/code&gt; and &lt;code&gt;__aexit__&lt;/code&gt;) to ensure the &lt;code&gt;aiohttp.ClientSession&lt;/code&gt; is created and properly closed, preventing resource leaks.&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;# async_client.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;span class="c1"&gt;# ... imports ...
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... init ...
&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;__aenter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AsyncClient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&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;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&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;self&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;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_tb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;self&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&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;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&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="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows concurrent execution, where the total time is the maximum delay, not the sum:&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;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fxmacrodata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncClient&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Concurrent calls are now trivial
&lt;/span&gt;        &lt;span class="n"&gt;data_aud&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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;aud&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;inflation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data_eur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&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;eur&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;gdp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;aud_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eur_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_aud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_eur&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;h2&gt;
  
  
  4. Utility: Cleaning Up the Data
&lt;/h2&gt;

&lt;p&gt;Data consumers expect chronologically sorted data, but APIs don't always guarantee it. A small utility function ensures the output is always clean time-series data, checking for either a &lt;code&gt;'date'&lt;/code&gt; or &lt;code&gt;'release_date'&lt;/code&gt; key.&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;# utils.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_by_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sorts a list of indicator data dictionaries by &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release_date&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&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;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&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;release_date&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;p&gt;Building this wrapper solidified my understanding of &lt;strong&gt;Object-Oriented Design&lt;/strong&gt;, the crucial performance trade-offs between &lt;strong&gt;Synchronous vs. Asynchronous&lt;/strong&gt; networking, and the importance of a great &lt;strong&gt;Developer Experience&lt;/strong&gt; through custom exceptions. You can explore the full source code on &lt;a href="https://github.com/fxmacrodata/fxmacrodata" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The final step was packaging the library and publishing it to PyPI. If you're building a tool to integrate real-time FX macro data, or just want a pattern for creating your own wrapper, the structure of this library is a solid foundation. Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Rob @ &lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fxmacrodata" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>How SuburbStory Uses Asyncio to Pull Multi-Source Government Data in Parallel</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Mon, 24 Nov 2025 07:16:16 +0000</pubDate>
      <link>https://dev.to/roberttidball/how-suburbstory-uses-asyncio-to-pull-multi-source-government-data-in-parallel-28d8</link>
      <guid>https://dev.to/roberttidball/how-suburbstory-uses-asyncio-to-pull-multi-source-government-data-in-parallel-28d8</guid>
      <description>&lt;p&gt;&lt;a href="https://suburbstory.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=asyncio_multi_source_data" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; publishes local updates for every suburb in NSW. The data behind each update comes from several government and public feeds: police, fire, traffic, transport, and various agency alerts. Each of these sources is independent, updated on its own schedule, and exposed through completely different HTTP endpoints.&lt;/p&gt;

&lt;p&gt;Because of that variety, the core engineering problem isn’t processing the data. It’s getting it fast enough, from all sources, without creating a slow and fragile chain of sequential API calls. The entire workload is I/O: pulling remote webpages, parsing lightweight text, and sending the processed information to Gemini to generate suburb-level summaries.&lt;/p&gt;

&lt;p&gt;The only way to make this practical at state-wide scale is to run everything in parallel. That’s where Python’s asyncio becomes the centre of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Sequential Fetching
&lt;/h2&gt;

&lt;p&gt;If you fetch each source one at a time, you create an artificial bottleneck. A single slow feed (for example a public incident feed that occasionally takes a few seconds to respond) blocks all other data from being processed. When you multiply that across hundreds of suburbs, you lose freshness and your cron job runtime becomes unpredictable.&lt;/p&gt;

&lt;p&gt;But none of these tasks depend on each other. Each fetch is just an isolated HTTP call. There’s no reason to wait for one to finish before starting the next.&lt;/p&gt;

&lt;p&gt;Using asyncio.gather to Hit Every Source at Once&lt;br&gt;
The actual workflow is simple: as soon as the cron job starts, every data source is queried in parallel. Instead of a loop like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for source in sources:
    data = fetch(source)
    process(data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the pipeline builds a list of coroutines and launches them simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;results = await asyncio.gather(*tasks, return_exceptions=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request goes out at the same time. Each one resolves whenever the remote server responds. A slow feed no longer slows down anything else. A fast feed is processed immediately. When you have tens of government sources multiplied across thousands of suburbs, this difference is enormous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing and Grouping on the Fly
&lt;/h2&gt;

&lt;p&gt;Once the fetches complete, the content is parsed and normalised. These steps are lightweight — mostly HTML extraction, field cleaning, text slicing, and location matching — so they run inline without blocking the loop. By the time all tasks in the gather call have resolved, you already have a full snapshot of every government update that matters for that cycle.&lt;/p&gt;

&lt;p&gt;The next step is grouping by suburb. Because all sources arrive together, the system can assemble a complete picture of what happened in a suburb across police, fire, traffic, and other feeds in a single pass. That grouped data becomes the input to the NLG stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Model Requests to Gemini
&lt;/h2&gt;

&lt;p&gt;Model calls are also I/O. They involve sending the suburb-level dataset to Gemini and waiting for a generated summary. Running these sequentially would be even slower than sequential scraping. But they fit the same pattern as the fetches: independent, high-latency network operations.&lt;/p&gt;

&lt;p&gt;So the pipeline builds another batch of tasks (one model call per suburb) and fires them all at once using asyncio.gather, with concurrency limits where needed. While the model is working on one suburb, dozens of other tasks are in flight. No time is spent waiting for NLG work to complete before starting the next one.&lt;/p&gt;

&lt;p&gt;This effectively compresses what would be minutes of serialized model calls down to the latency of a single batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entire cycle looks like this:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All data sources fetched concurrently&lt;/li&gt;
&lt;li&gt;All parsing done immediately after responses arrive&lt;/li&gt;
&lt;li&gt;All suburb summaries generated concurrently&lt;/li&gt;
&lt;li&gt;All output stored without blocking the loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cron job doesn’t wait on anything except the slowest item in each batch. Everything else is overlapped. Even though the system touches many external endpoints and calls an LLM at scale, the total runtime stays short and predictable because every part of the pipeline runs in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Asyncio Suits This Problem Exactly
&lt;/h2&gt;

&lt;p&gt;The workload behind &lt;a href="https://suburbstory.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=asyncio_multi_source_data" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; isn’t CPU-heavy. There’s no local ML model, no heavy processing, no long transformations. It’s almost entirely network-bound. Asyncio is designed for this exact shape of problem: many independent I/O operations that can safely run together.&lt;/p&gt;

&lt;p&gt;Using threads or processes would add overhead and resource waste. Using synchronous requests would stretch each cron cycle unnecessarily. Asyncio sits neatly in the middle with minimal overhead, maximum concurrency, and predictable performance.&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>ai</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Flexible Flask Endpoints for Hierarchical Local News on SuburbStory</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Mon, 17 Nov 2025 03:40:03 +0000</pubDate>
      <link>https://dev.to/roberttidball/flexible-flask-endpoints-for-hierarchical-local-news-on-suburbstory-3dd8</link>
      <guid>https://dev.to/roberttidball/flexible-flask-endpoints-for-hierarchical-local-news-on-suburbstory-3dd8</guid>
      <description>&lt;p&gt;&lt;a href="https://suburbstory.com/?utm_source=hashnode&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; publishes automated local news pages for every suburb in NSW (Australia). The site’s URLs follow a clear geographic hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://suburbstory.com/&amp;lt;country&amp;gt;/&amp;lt;state&amp;gt;/&amp;lt;area&amp;gt;/&amp;lt;suburb&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page is built by aggregating structured data from multiple government sources such as police, fire, traffic, and council feeds. This data is then processed via the Gemini API to generate readable content.&lt;/p&gt;

&lt;p&gt;A key challenge is &lt;strong&gt;efficiently mapping thousands of geographic URL combinations to a single Flask application&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dynamic Routing in Flask
&lt;/h2&gt;

&lt;p&gt;Rather than creating a route for each suburb, SuburbStory uses a single, flexible Flask route:&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.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&amp;lt;country&amp;gt;/&amp;lt;state&amp;gt;/&amp;lt;area&amp;gt;/&amp;lt;suburb&amp;gt;/&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;suburb_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&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;area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suburb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country&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;state&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;area&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;suburb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_suburb_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fetch data from Firestore
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suburb.html&lt;/span&gt;&lt;span class="sh"&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flask automatically parses the URL segments and passes them as function arguments. The handler then retrieves the corresponding Firestore document and renders it with a single template. This allows one route to serve thousands of unique pages across different areas and suburbs without modifying the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Examples
&lt;/h3&gt;

&lt;p&gt;Two suburbs under the same area illustrate the approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://suburbstory.com/au/nsw/willoughby/chatswood/" rel="noopener noreferrer"&gt;https://suburbstory.com/au/nsw/willoughby/chatswood/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://suburbstory.com/au/nsw/willoughby/artarmon/" rel="noopener noreferrer"&gt;https://suburbstory.com/au/nsw/willoughby/artarmon/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These pages use the same route and template while aggregating their own data, demonstrating how hierarchical Flask endpoints efficiently handle multiple suburbs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Hierarchical URLs Matter
&lt;/h2&gt;

&lt;p&gt;The hierarchical pattern &lt;code&gt;country/state/area/suburb&lt;/code&gt; has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictable and SEO-friendly URLs&lt;/strong&gt; – easy to link externally and for search engines to index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct mapping to Firestore&lt;/strong&gt; – each page can be stored under a document key like &lt;code&gt;"au/nsw/willoughby/chatswood"&lt;/code&gt;, simplifying retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template reusability&lt;/strong&gt; – the same &lt;code&gt;suburb.html&lt;/code&gt; template dynamically adjusts based on the URL variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable architecture&lt;/strong&gt; – Cloud Run handles all routes in one Flask app, allowing horizontal scaling without additional routing configuration.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaways for Python Developers
&lt;/h2&gt;

&lt;p&gt;SuburbStory demonstrates how flexible Flask routing enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serving thousands of hierarchical pages from a single route&lt;/li&gt;
&lt;li&gt;Efficiently handling independent data fetches per page&lt;/li&gt;
&lt;li&gt;Scaling a serverless Flask app on Cloud Run without duplicating code&lt;/li&gt;
&lt;li&gt;Maintaining clean, predictable, SEO-friendly URLs that map directly to Firestore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a practical model for building large-scale local or multi-tenant content systems in Python.&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>google</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🚀 Scaling Up: Why I Chose FastAPI Over Flask and Django for a Data API</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Thu, 13 Nov 2025 04:23:02 +0000</pubDate>
      <link>https://dev.to/roberttidball/scaling-up-why-i-chose-fastapi-over-flask-and-django-for-a-data-api-1phi</link>
      <guid>https://dev.to/roberttidball/scaling-up-why-i-chose-fastapi-over-flask-and-django-for-a-data-api-1phi</guid>
      <description>&lt;p&gt;As a developer, when you set out to build a new data service—like my recent project, &lt;strong&gt;FXMacroData&lt;/strong&gt; (check out the live API at &lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=fastapi_choice" rel="noopener noreferrer"&gt;FXMacroData - Real-time Forex Data&lt;/a&gt;) the framework choice is crucial. My goal was simple: serve high-frequency macroeconomic data instantly and reliably.&lt;/p&gt;

&lt;p&gt;I narrowed the field down to the Python giants: &lt;strong&gt;Flask&lt;/strong&gt;, &lt;strong&gt;Django&lt;/strong&gt;, and &lt;strong&gt;FastAPI&lt;/strong&gt;. While Flask is famously lightweight and Django is the enterprise powerhouse, I ultimately landed on &lt;strong&gt;FastAPI&lt;/strong&gt;. Here's why that decision was a game-changer for building a performant, modern data API designed for the cloud.&lt;/p&gt;




&lt;h3&gt;
  
  
  The API Mandate: Speed, Concurrency, and Serverless
&lt;/h3&gt;

&lt;p&gt;The core requirement for the FXMacroData API is high &lt;strong&gt;concurrency&lt;/strong&gt; and an architecture optimized for modern cloud deployments, specifically &lt;strong&gt;serverless&lt;/strong&gt; environments like &lt;strong&gt;Google Cloud Run&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flask (Sync):&lt;/strong&gt; Standard Flask is synchronous (WSGI), meaning it blocks a worker thread while waiting for I/O (like a database query). This inefficiency makes it harder to scale cost-effectively in a serverless environment where every millisecond of CPU time matters.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django (Monolithic):&lt;/strong&gt; Django is excellent, but it’s a &lt;strong&gt;heavyweight framework&lt;/strong&gt;. For a pure API backend, its many built-in components were simply &lt;strong&gt;overkill&lt;/strong&gt;. Deploying a massive framework just to serve a few data endpoints is inefficient, especially when using a flexible NoSQL backend like &lt;strong&gt;Firestore&lt;/strong&gt;. Its complexity and synchronous default were hurdles to fast, cost-effective serverless deployment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚡️ FastAPI: Natively Async for Cloud Run
&lt;/h3&gt;

&lt;p&gt;FastAPI is built on the modern &lt;strong&gt;ASGI standard&lt;/strong&gt;, making it asynchronous (&lt;strong&gt;async/await&lt;/strong&gt;) from the ground up. This was the single biggest performance advantage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Non-Blocking I/O:&lt;/strong&gt; Most API operations are I/O-bound (waiting for the database or network). Because FastAPI's worker doesn't block, it can efficiently handle hundreds of concurrent requests using minimal resources. This is essential for &lt;strong&gt;Cloud Run's scaling model&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless Integration:&lt;/strong&gt; Being lightweight and ASGI-native means FastAPI runs perfectly within the brief lifespan of a serverless container. It integrates cleanly with external services like &lt;strong&gt;Firestore&lt;/strong&gt;, making it the ideal choice for a &lt;strong&gt;stateless, instant-scaling microservice&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  The Code Difference: I/O Concurrency
&lt;/h3&gt;

&lt;p&gt;The benefit of native asynchronous programming is immediately clear when requesting data from multiple sources.&lt;/p&gt;

&lt;h4&gt;
  
  
  ➡️ Flask (Synchronous/Blocking)
&lt;/h4&gt;

&lt;p&gt;The total execution time is the &lt;strong&gt;sum&lt;/strong&gt; of the two delays (approx. 2 seconds), as the second call must wait for the first to complete.&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;# Flask (Synchronous)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&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="c1"&gt;# Execution runs sequentially: A waits for B to finish.
&lt;/span&gt;&lt;span class="nd"&gt;@app.route&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_example&lt;/span&gt;&lt;span class="p"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source A
&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source B
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Time: ~2.0s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ➡️ FastAPI (Asynchronous/Non-Blocking)
&lt;/h4&gt;

&lt;p&gt;The total execution time is the &lt;strong&gt;maximum&lt;/strong&gt; of the two delays (approx. 1 second), as both I/O operations are initiated concurrently.&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;# FastAPI (Asynchronous)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&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;FastAPI&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Execution runs concurrently: A and B start at the same time.
&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;/&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;async_example&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;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source A
&lt;/span&gt;        &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Wait for Source B
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Time: ~1.0s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧠 The Productivity Bonus: Clean Code and Auto-Docs
&lt;/h3&gt;

&lt;p&gt;Beyond performance and cloud architecture, FastAPI delivered on developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses &lt;strong&gt;Pydantic models&lt;/strong&gt; and &lt;strong&gt;Python type hints&lt;/strong&gt; for automatic data validation and serialization.
&lt;/li&gt;
&lt;li&gt;It generates &lt;strong&gt;interactive OpenAPI documentation (Swagger UI)&lt;/strong&gt; automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, FastAPI allowed me to build a high-performance, stateless API that perfectly matches the pay-per-use efficiency of Google Cloud Run, making it technically superior and far more cost-effective than using an over-engineered framework like Django for this specific task.&lt;/p&gt;

&lt;p&gt;If you're building a new API focused on speed, data movement, and cloud-native scaling, the choice is clear: &lt;strong&gt;go async with FastAPI.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>fastapi</category>
      <category>gcp</category>
    </item>
  </channel>
</rss>
