Applying API Testing Frameworks: A Comparative Guide
Welcome to this comprehensive comparative guide on API testing! This article is designed to showcase...
找到 563 篇相关文章
Welcome to this comprehensive comparative guide on API testing! This article is designed to showcase...
Every big social network locks audience data behind auth walls and anti-bot systems. Bluesky went the other way. The AT Protocol is open by design, so public profile data (bios, follower counts, full follower and following lists) is queryable through a documented API without logging in. The whole surface is basically two endpoints: GET https://api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=HANDLE GET https://api.bsky.app/xrpc/app.bsky.graph.getFollowers?actor=HANDLE&limit=100 There's also getProfiles for batching 25 handles per call. Follower lists paginate with a normal cursor , which still works on the graph endpoints. Search is a different story, cursor pagination 403s there now, but that's a topic for another post. For one-off lookups, curl is honestly all you need. Where it gets tedious Bulk. Thousands of profiles, follower exports that run into six figures, weekly snapshots for tracking. Pagination, rate-limit backoff, and stitching the pages together is boring code that has to run reliably. I packaged that part as an Apify actor: Bluesky Profile Scraper . Paste handles or profile URLs, optionally turn on follower/following export, and you get JSON or CSV back with a sourceProfile field linking each follower record to the profile it belongs to. $2 per 1,000 records, runs on a schedule if you want snapshots over time. What people use this for Vetting an influencer's real audience before paying them. Exporting who follows a competitor and what their bios say. Charting follower growth from weekly runs. And enrichment: find who's talking about you with a mentions monitor , then profile those authors to see their actual reach. Bluesky is the only major network right now where any of this is straightforward and stable. Worth using while it lasts.
Ask ChatGPT or Perplexity "what's the best note-taking app" and you get a shortlist of three to five names. Either you're on it or you don't exist in that channel. And buying research keeps moving there. People call measuring this GEO or AEO tracking now. The way most teams do it is pasting questions into chatbots by hand and eyeballing the answers. That stops scaling at about ten questions, and you can't trend it week over week. Doing it programmatically Don't scrape the chat UIs. It's fragile, against ToS, and breaks weekly. The engines all have official APIs with web search: Perplexity's sonar models return answers with citations built in OpenAI has gpt-4o-search-preview for live web search Gemini's gemini-2.5-flash supports Google Search grounding One OpenRouter key covers all three through a single endpoint, which keeps the code boring. For each buyer question you care about, record four things per engine: was the brand mentioned, how early in the answer, was your domain cited as a source, and how often competitors appeared. That last one gives you share of voice. The packaged version I built this as an Apify actor: AI Brand Visibility Tracker . You give it a brand name, domain, competitors, and topics. It generates realistic buyer questions and returns one JSON row per check: brandMentioned , positionScore , brandCited , shareOfVoice , citedDomains , plus a per-engine summary. Schedule it weekly and you have an AI visibility trendline for client reports. $0.05 per check. The field that actually matters citedDomains is the actionable one. It tells you which sites the AI engines treat as sources for your category. Getting mentioned on those specific domains is how you move your visibility. It's link building, except the target list comes from the AI's own citations instead of a guess.
I wanted to know when people mention a brand on Bluesky. Simple ask. Turns out Brandwatch, Mention, Hootsuite, basically every social listening tool, still doesn't cover it. They're all busy with X and Instagram while Bluesky sits at 27M+ monthly users. So I looked at doing it myself and found out something most people miss: you don't need to scrape anything. Bluesky runs on the AT Protocol, which is open by design. Public posts are searchable through a documented endpoint. No login, no API key. GET https://api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=YOUR_BRAND&sort=latest&limit=100 That returns full post objects. Text, author handle, timestamps, like/repost/reply counts, embedded links, hashtags. Everything you need. Two things that broke my first version Worth writing down because most tutorials get this wrong now: The public.api.bsky.app host that older guides point to returns 403 for search. Use api.bsky.app instead. As of July 2026, unauthenticated search rejects cursor pagination. Page one works fine, page two gets you a 403 with "request forbidden by administrative rules". The nasty part is it looks like rate limiting, but it isn't. The workaround: paginate by time. Use sort=latest , then pass until= with the createdAt of the oldest post from the previous page. Dedupe on uri because the boundary post shows up twice. If you don't want to maintain any of that I packaged the whole job as an Apify actor: Bluesky Mentions Scraper . Keywords in, clean JSON out. It handles the pagination and retry stuff above, filters replies if you want, scores basic sentiment, and can pull follower counts for each author so you can sort mentions by reach. Runs on a schedule, exports CSV, plugs into Slack or n8n through Apify's integrations. It also works as an MCP tool inside Claude or Cursor. Pricing is per result, $4 per 1,000 mentions. No subscription. What I actually monitor Brand and product names plus the common misspellings. Competitor names, because share of voice on Blu
From MVP to Enterprise: Architecting AI APIs That Don't Fail at 3AM I've been on-call for enough production incidents to know that the difference between a startup's AI integration and an enterprise one isn't just budget. It's everything downstream — your p99 latency, your failover story, the size of your blast radius when a provider has a bad Tuesday. Most guides lump these two worlds together and that's exactly why teams end up rearchitecting at the worst possible moment. Let me walk you through how I think about it now, after spending years shipping LLM-backed services for both early-stage teams and Fortune 500 procurement departments. The short version: I almost always route through Global API, and the tier I pick depends entirely on what keeps me up at night. The Question Nobody Asks First: What Breaks When? When I sit down with a founder, the conversation usually starts with "which model should we use?" That's the wrong first question. The right first question is: what's your tolerance for a 3 a.m. page? If you're a seed-stage startup with a handful of users, your answer is probably "none, but I'll deal with it." If you're a publicly traded company processing loan applications, your answer is "I need a 99.9% SLA in writing, multi-region failover, and a support escalation path that doesn't start with a Discord server." Those two answers produce two completely different architectures. Let me show you what I mean. The Startup Reality: Speed and Optionality Here's the dirty secret about direct provider integration for startups: it feels free, and then it isn't. I watched a team burn six weeks trying to wire up DeepSeek's API directly. They needed a Chinese phone number for verification, an Alipay or WeChat account for payment, and they were stuck the moment they wanted to A/B test against Qwen or another model. Their CTO told me afterward, "We spent a sprint on payment infrastructure before we shipped a single feature." That pain compounds. Every new model is a ne
We’ve all been there. You have a "Leg Day" scheduled in your Notion database, but you woke up feeling like a truck hit you. Your Apple Watch says your Heart Rate Variability (HRV) is in the gutter, but your rigid calendar doesn't care. Usually, you’d either push through and risk injury or manually move cards around in Notion—which is a friction-filled nightmare. In this tutorial, we are building a Self-Optimizing Health Agent using LangGraph , Notion API , and HealthKit . This agent acts as a closed-loop system: it analyzes your physiological recovery data, reasons about your physical state using an LLM, and automatically rewrites your training schedule. By mastering AI agents , LLM orchestration , and fitness automation , you’ll turn your static "To-Do" list into a dynamic "Should-Do" list. 🥑 The Architecture: The Bio-Feedback Loop Using LangGraph , we can treat our fitness logic as a state machine. Unlike a linear script, a graph allows our agent to decide whether it needs to fetch more context (like yesterday's sleep) before making a final decision on your workout. graph TD Start((Start)) --> FetchHRV[Fetch HRV Data via HealthKit] FetchHRV --> CheckRecovery{LLM: Analyze Recovery} CheckRecovery -- "Low Recovery (Fatigued)" --> ModifyNotion[Action: Downgrade Workout Intensity] CheckRecovery -- "High Recovery (Fresh)" --> KeepNotion[Action: Maintain/Boost Intensity] ModifyNotion --> UpdateNotion[Update Notion Page] KeepNotion --> UpdateNotion UpdateNotion --> End((Done)) style CheckRecovery fill:#f96,stroke:#333,stroke-width:2px style FetchHRV fill:#bbf,stroke:#333 Prerequisites Before we dive into the code, ensure you have: Python 3.10+ LangChain & LangGraph installed ( pip install langgraph langchain_openai ) Notion Integration Token (with access to your workout database) HealthKit SDK (Note: Since we are in a Python environment, we'll simulate the HealthKit fetcher, though in a real-world scenario, this would be bridged via a FastAPI endpoint from an iOS app). St
If you build apps for jewelers, fintech dashboards, or e-commerce price automation, you eventually need one thing: reliable, low-latency gold and currency prices . Scraping fragile sources breaks constantly. A dedicated price API solves this. In this post I'll show how to consume real-time gold (gram, quarter, coin) and FX rates over both REST and WebSocket (Socket.IO) using the Hasfiyat Gold & Currency API . Why a price API instead of scraping? Stability — a documented contract instead of HTML that changes without notice. Low latency — prices are pushed as the market moves, not on a slow cron. Multiple sources with failover — if one provider drops, the feed keeps flowing. 1. Polling with REST The simplest integration: request the prices you need with your API key. curl -X GET \ 'https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Accept: application/json' // Node.js const res = await fetch ( " https://api.hasfiyat.com/api/prices?symbols=HAS,GRAM,CEYREK " , { headers : { Authorization : " Bearer YOUR_API_KEY " } } ); const data = await res . json (); console . log ( data ); REST is ideal for periodic reporting, server-side jobs, and updating e-commerce product prices. 2. Live updates with Socket.IO For price screens, signage, and mobile apps where every tick matters, keep a connection open and let the server push changes: import { io } from " socket.io-client " ; const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR_API_KEY " } }); socket . on ( " gold_prices " , ( data ) => { // { symbol: "HAS", type: "Has Altın", buy: 2450.85, sell: 2455.10, timestamp: "14:32:01.045" } console . log ( data ); }); No polling, no hammering the server — each market move arrives instantly. 3. A minimal live ticker in the browser <div id= "gold" ></div> <script src= "https://cdn.socket.io/4.7.5/socket.io.min.js" ></script> <script> const socket = io ( " https://api.hasfiyat.com " , { auth : { token : " YOUR
If you want remote job data, you do not need to scrape HTML or sign up for anything. Four of the bigger remote job boards publish keyless public feeds. The catch is that they all speak different dialects, so the real work is normalization. Here are the endpoints and the traps. The four feeds RemoteOK returns its whole current board as one JSON array: GET https://remoteok.com/api The first element is a legal notice, not a job: they ask for a link back with attribution as a condition of using the feed. Skip element zero, and honor the attribution if you republish. Jobs carry salary_min and salary_max as numbers, tags, and ISO dates. Remotive has the friendliest API of the four, including server side search: GET https://remotive.com/api/remote-jobs?search=python&limit=100 Salary here is free text ( "$120k - $160k" ), so do not expect numbers. Attribution with a link back is required here too. WeWorkRemotely publishes RSS: GET https://weworkremotely.com/remote-jobs.rss Two quirks: the company name is not a field, it is baked into the title as Company: Role , so split on the first colon. And useful data hides in nonstandard tags like <region> , <skills> , and <category> that generic RSS parsers drop on the floor. Himalayas has a proper paginated API with a surprisingly deep catalog (100k+ listings): GET https://himalayas.app/jobs/api?limit=100&offset=0 It gives structured minSalary / maxSalary with a currency and period, seniority arrays, location restrictions, and even timezone restrictions as UTC offsets. Dates are epoch seconds, not ISO strings. The normalization layer The row schema that survived contact with all four sources: { "source" : "Remotive" , "title" : "Senior Backend Engineer" , "company" : "Acme Corp" , "tags" : [ "python" , "aws" ], "salaryMin" : null , "salaryMax" : null , "salaryText" : "$120k - $160k" , "location" : "Worldwide" , "postedAt" : "2026-07-03T20:01:13.000Z" , "applyUrl" : "https://..." } Rules that mattered in practice: Keep both salary sh
Bruno — API Client แบบ Git-Native ที่เก็บทุกอย่างเป็นไฟล์ เวลา dev team ต้องเทส API — เครื่องมือที่ทุกคนนึกถึงคือ Postman กับ Insomnia แต่ปัญหาคลาสสิกที่เจอกันแทบทุกทีม: "Postman collection อยู่ไหน?" — "ใน account ผมไง" "ขอ invite หน่อย" — "เดี๋ยวส่ง link ให้... เอ๊ะ หมด free tier แล้ว" นี่คือ pain point ที่ทำให้คนจำนวนมากมองหาเครื่องมือใหม่ — และหนึ่งในนั้นคือ Bruno Bruno คืออะไร Bruno เป็น API client แบบ desktop app (มีทั้ง macOS, Linux, Windows) ที่มีแนวคิดแตกต่างจาก Postman โดยสิ้นเชิง: Postman Bruno เก็บข้อมูลที่ไหน Cloud account ไฟล์ใน project (Git repo) ต้อง login ไหม ✅ ต้อง ❌ ไม่ต้อง Collection format JSON (binary-ish) Plain text (Bru files) Collaborate ผ่าน Postman cloud ผ่าน Git (PR, diff, review) Open source ❌ ✅ (GitHub: 45K+ stars) Offline ไม่ค่อยได้ ✅ ทำงานออฟไลน์ได้เต็มที่ หัวใจของ Bruno คือ "API Client ไม่ใช่ Platform" — มันคือเครื่องมือธรรมดาที่เก็บข้อมูลเป็นไฟล์ — เหมือนที่ dev ทั่วไปเก็บโค้ด จุดเด่น 1. Collection คือไฟล์ — เก็บใน Git ได้ my-project/ ├── src/ ├── bruno/ │ ├── users/ │ │ ├── GET users.bru │ │ ├── POST create user.bru │ │ └── DELETE user.bru │ ├── auth/ │ │ └── POST login.bru │ └── bruno.json └── .git/ ทุก API request เป็นไฟล์ .bru — plain text — diff ได้, PR review ได้, merge ได้ — เหมือนโค้ด meta { name: GET users type: http seq: 1 } get { url: https://api.example.com/users body: none auth: bearer } 2. ไม่มี Cloud — ข้อมูลอยู่กับคุณ Bruno ไม่เคยส่งข้อมูลขึ้น server — ทุกอย่างอยู่บนเครื่องคุณ ทั้ง request, response, environment variables สำหรับทีมที่ทำงานกับข้อมูล sensitive (banking, healthcare, government) — ข้อนี้สำคัญมาก 3. ใช้ Git เป็น Collaboration Tool แทนที่จะ "invite teammate เข้า workspace" (แบบ Postman) — คุณแค่: git add bruno/ git commit -m "add user API collection" git push เพื่อน git pull → เปิด Bruno → เห็น collection เดียวกันทันที 4. Environment Variables — แบบเดียวกับที่ dev ใช้ # environments/production.bru vars { base_url : https : //api.production.com api_key : {{ PROD_API_KEY }} } เปลี่ยน environment ด้วยการคลิก —
Google Trends has no official API, and most wrapper libraries rot within months. But the Trends site itself runs on a keyless JSON API that anyone can call, and it serves the exact numbers you see in the UI. Here is the full recipe, including one gotcha where Google quietly labels your session a scraper. The two step flow Trends works in two steps. First you call explore , which returns a list of widgets, one per chart on the page, each with a signed token: GET https://trends.google.com/trends/api/explore ?hl=en-US&tz=0 &req={"comparisonItem":[{"keyword":"web scraping","geo":"US","time":"today 12-m"}],"category":0,"property":""} Then you call a widget data endpoint with that widget's request and token : GET https://trends.google.com/trends/api/widgetdata/multiline?hl=en-US&tz=0&req=<widget.request>&token=<widget.token> The widget kinds map to endpoints: TIMESERIES uses multiline , GEO_MAP uses comparedgeo , and both RELATED_QUERIES and RELATED_TOPICS use relatedsearches . The cookie trick Call explore cold and you get a 429. The API wants a NID cookie, and here is the counterintuitive part: you get it by requesting the public explore page first, and that page may itself respond 429 while still setting the cookie you need. const res = await fetch ( ' https://trends.google.com/trends/explore?geo=US&q=test ' ); // res.status may be 429. The Set-Cookie header is still there. const cookies = res . headers . getSetCookie (); Grab the cookie from the 429 response, retry explore , and everything works. Strip the anti JSON prefix Every Trends response starts with a junk line like )]}' to break naive JSON.parse calls. Drop everything up to the first newline: const body = await res . text (); const data = JSON . parse ( body . slice ( body . indexOf ( ' \n ' ) + 1 )); The scraper flag Here is the part I have not seen documented. Look inside the widget request object that explore returns to a keyless session: "userConfig" : { "userType" : "USER_TYPE_SCRAPER" } Google knows. And
You wire up the OpenAI SDK, ship the feature, and it works. Three weeks later someone in finance forwards a screenshot of a bill that tripled and asks what happened. You open the provider dashboard, see one big number, and… that's it. No per-feature breakdown, no idea which change caused it, no way to tell whether it's a bug or just growth. I've watched this happen at enough teams that I now treat "we can't explain our AI bill" as a predictable stage every company hits about two months after their first LLM feature ships. Here's how to get ahead of it — starting with plain code, then the tradeoffs, then where a dedicated tool actually earns its keep. Disclosure up front: I work on StackSpend, which does the full version of this. I've kept the first 80% of this post vendor-neutral because most of it you can and should build yourself before you buy anything. The core problem: the bill is a single number, your costs are not Provider dashboards give you total spend over time. What you actually need to make decisions is spend broken down by the dimensions you care about: Per feature — is it the summarizer or the chat assistant that's expensive? Per customer / tenant — which accounts cost more to serve than they pay? Per model — how much are you spending on GPT-4-class vs cheaper models? Per environment — is a runaway staging job quietly burning money? None of those dimensions exist in the raw bill. You have to attach them yourself, at call time, because after the request is gone the context is gone with it. Step 1: capture usage at the call site Every major provider returns token usage in the response. The trick is to log it with your own business context attached — the feature name, the tenant, the environment. Here's the pattern in TypeScript with the OpenAI SDK: import OpenAI from " openai " ; const openai = new OpenAI (); // Prices per 1M tokens — keep these in config, they change often. const PRICING : Record < string , { input : number ; output : number } > = { " g
Every week, over 1,300 US companies file a funding round with the SEC — and most of them never appear in the tech press. If your job involves selling to funded startups, tracking competitors' war chests, or spotting investment trends, you're probably relying on funding newsletters and databases that are days late and hundreds of dollars per seat. There's a better way: go to the primary source. In this tutorial you'll build a real-time startup funding feed from SEC Form D filings — the regulatory document every US company must file when it raises private capital. You'll get exact amounts, industries, locations and investor counts, as clean JSON, for a fraction of a cent per round. Why Form D beats funding news When a startup raises money under Regulation D (the exemption used by virtually all US venture rounds), it must file Form D with the SEC within 15 days of the first sale. That filing includes: The exact amount sold so far — not a journalist's "sources say" estimate The total offering size (or whether it's open-ended) Industry group, city and state Number of investors who participated Date of first sale and year of incorporation Compare that to funding news: TechCrunch covers a tiny, PR-driven slice. Databases like Crunchbase aggregate press and manual research — comprehensive over time, but late and expensive. Form D is the ground truth both of them chase. The catch? EDGAR (the SEC's database) is built for lawyers, not for automation. The filings are XML documents scattered across an archive, discoverable only through a quirky full-text search API with hidden rate limits. That's the part we'll automate. The 5-minute setup We'll use the Startup Funding Feed Actor — it handles EDGAR's discovery API, XML parsing, rate limits and pagination, and returns one JSON record per filing. It's pay-per-event: $0.002 per filing returned (a full weekly sweep of all US rounds costs ~$2.60; a filtered slice costs cents). Failed fetches are never charged. Create a free Apify acc
I spent the last while building the HMRC integration for TapTax , a Making Tax Digital (MTD) app for UK sole traders. MTD is the UK government's programme that pushes tax filing out of paper and spreadsheets and into software talking directly to HMRC's APIs. I have integrated with a fair few third-party APIs. Stripe, Plaid-style banking, the usual. HMRC is its own animal. Some of it is genuinely well designed, some of it caught me completely off guard, and a couple of things cost me a full day each before the penny dropped. So here are the five things that surprised me most. Each one is the surprise, then the fix, with a short snippet from our actual TypeScript backend. Not tax advice, just engineering notes from someone who has now stepped on the rakes so you do not have to. 1. The API version lives in the Accept header, and getting it wrong is a 406 Most APIs version in the URL: /v2/thing . HMRC versions through content negotiation. You ask for a version in the Accept header, like application/vnd.hmrc.5.0+json , and if you ask for a version that endpoint does not serve, you get a 406 Not Acceptable . No helpful "did you mean v3" message. Just 406. The part that bit me: different endpoints are on completely different versions at the same time. Obligations is on v3.0, the self-employment cumulative summary is on v5.0, calculations are on v8.0, ITSA status is on v2.0. There is no single "current" version to pin. The fix was to make the version a required argument on the request wrapper so you can never forget it, and set it per call: // src/services/hmrcApi.ts const headers = { Authorization : `Bearer ${ accessToken } ` , Accept : `application/vnd.hmrc. ${ apiVersion } +json` , // e.g. "5.0" ... hmrcConfig . getFraudHeaders ( req ), }; One more trap: versions get withdrawn. Obligations used to answer on v2.0; that now returns a 404, not a 406, so it looks like a missing resource rather than a stale version. When an HMRC call 404s, check the version before you go hunt
Last week someone tried to copy my visa API's database. They didn't succeed — they got 0.6% of it before I cut the key — but the 251 requests they left behind are a near-perfect teaching case for what targeted API extraction actually looks like from the defender's side. Here's the forensic walkthrough. The target One endpoint: GET /api/v1/visa?from={passport}&to={destination} It returns the visa rule for a passport→destination pair — visa type, allowed stay, conditions. The full matrix is ~39,585 pairs . That matrix is the product. The evidence The attacker's requests weren't spread across the map. They were a sweep, one passport at a time: Passport Destinations pulled Coverage 🇦🇪 UAE (ARE) 195 ~100% of that passport's matrix 🇦🇺 Australia (AUS) 53 ~1/4, interrupted 🇨🇳 China (CHN) 2 test calls 249 unique pairs, near-zero duplicates. Whoever wrote this was methodical: validate that one full passport comes out cleanly, then move to the next. Reading the cadence The timestamps are where a scrape gives itself away. Minute by minute: 11:56 2 ← test phase (incl. the one failure) 11:57 1 11:58 25 ┐ 11:59 26 │ 12:00 20 │ ~25 req/min, dead regular … │ = one request every ~2.4s 12:07 21 ┘ No human reads visa rules on a 2.4-second metronome for 11 minutes. This is a loop. The fingerprint Four signals — and the point isn't nationality, it's that the request parameters themselves leaked the intent: Handle: visadb_scraper . It signed its own work. Email: throwaway @temp.com . No intention of receiving anything. Languages: en + zh , on a product with no Chinese-market surface yet. Error signature: the very first call (CHN→THA, in Chinese, 11:56:45) failed, then everything ran clean. Classic "calibrating the script" tell. The math 250 records is 0.6% of the base. At 25 req/min, a full dump would've taken ~26 hours . This wasn't a dump — it was a feasibility test . They proved a whole passport comes out easily, then stopped, nowhere near the 3,000/month free-tier ceiling. What I coul
Selby's VC firm Copper Sky Capital is currently raising a $300 million second fund, according to a regulatory filing.
Everyone reads the Hugging Face trending page in a browser. Almost nobody knows the whole Hub sits behind a plain JSON API with no key, no login, and cursor pagination. If you want a weekly report of what the AI community is actually adopting, you can build it with fetch . The endpoints GET https://huggingface.co/api/models GET https://huggingface.co/api/datasets GET https://huggingface.co/api/spaces Useful parameters, same across all three: sort ranks results: trendingScore , downloads , likes , createdAt , lastModified direction=-1 for descending search matches names, author restricts to one org like meta-llama filter matches Hub tags: text-generation , license:mit , even arxiv:2606.23050 limit up to 100 per page So the top trending models right now: https://huggingface.co/api/models?sort=trendingScore&direction=-1&limit=100 trendingScore is the interesting one. Downloads and likes rank all time popularity, which is dominated by the same old models. Trending score is Hugging Face's own measure of current momentum, and it moves daily. Today it puts a four day old OCR model from Baidu at the top, which no downloads sort would surface for weeks. Slim payloads with expand By default the models endpoint returns a siblings array listing every file in the repo, which bloats a 100 item page. Ask for exactly the fields you want instead: const fields = [ ' downloads ' , ' likes ' , ' trendingScore ' , ' pipeline_tag ' , ' tags ' , ' createdAt ' ]; const params = new URLSearchParams ({ sort : ' trendingScore ' , direction : ' -1 ' , limit : ' 100 ' }); for ( const f of fields ) params . append ( ' expand[] ' , f ); const res = await fetch ( `https://huggingface.co/api/models? ${ params } ` ); const models = await res . json (); Pagination is a Link header There is no page parameter. Each response carries a Link header with a cursor for the next page, GitHub style: function nextUrl ( res ) { const m = ( res . headers . get ( ' link ' ) || '' ). match ( /< ([^ > ] + ) >; \s *r
In my previous article, I introduced the NeNe series: a family of small, self-hosted business tools for teams operating in Japan. Previous article: https://dev.to/hideyukimori/i-am-building-self-hosted-business-tools-for-small-teams-in-japan-4i26 This article is a deeper look at one of those tools: NeNe Records . Repository: https://github.com/hideyukiMORI/nene-records NeNe Records is an API-first typed CMS and flexible entity platform built on NENE2 , my small PHP framework for AI-readable business APIs. It is not trying to be a WordPress clone. It is not trying to run WordPress plugins or themes. The goal is different: manage business data and public content through typed schemas, documented APIs, and clear AI tool boundaries. Why not just use WordPress? WordPress is useful. It has proven that a generic content model can support blogs, pages, shops, media, and many small business workflows. But when I think about business data, a few problems keep coming back: untyped metadata plugin-specific data shapes hooks and filters that can change behavior from many places API contracts that depend heavily on plugins AI integrations that may not know where the real application boundary is The postmeta model is flexible, but it often becomes stringly typed storage. That is fine for many websites. But for business data, I usually want the API to know more: this field is text this field is an enum this field is an image this field is a relation this field is required this record belongs to this organization this operation requires this role That is the space where NeNe Records lives. Typed records instead of metadata chaos The core model is simple: Entity Type -> Field Definition -> Record An Entity Type defines what kind of data exists. Examples: posts pages products events internal documents A Field Definition describes the shape of a field: text markdown html blocks int enum bool datetime image file relation A Record is the actual data. The important part is that the schema
A company shifted from project- to product-thinking after their platform outgrew single-team use. The limitations that they felt with their platform were one-off deliveries, lack of product vision, and weak feedback loops. They have moved toward a self-service, API-driven, multi-tenant infrastructure with clearer ownership and better abstractions. By Ben Linders
Every blog post needs an OG image. Without one, your links look blank on Twitter, LinkedIn, and Slack — just a plain URL that nobody clicks. Most developers solve this by spinning up a headless browser, loading an HTML template, taking a screenshot, and uploading it somewhere. It works, but now you're maintaining a Puppeteer instance, dealing with font rendering quirks, and burning server resources on something that should be simple. There's a faster approach: design your OG images as HTML templates and let a screenshot API handle the rendering. The Idea: HTML Templates as OG Images Think of your OG image as a tiny webpage. You already know HTML and CSS. Build a 1200×630 template with your blog title, author name, maybe a gradient background — whatever fits your brand. Host it or pass it as raw HTML. Then call an API to screenshot it. Done. A basic template might look like this: <div style= "width:1200px;height:630px;display:flex;align-items:center; justify-content:center;background:linear-gradient(135deg,#1a1a2e,#16213e); font-family:Inter,sans-serif;padding:60px" > <div style= "color:#fff;text-align:center" > <h1 style= "font-size:48px;margin:0" > {{title}} </h1> <p style= "font-size:24px;color:#8892b0;margin-top:20px" > {{author}} · {{date}} </p> </div> </div> Replace the placeholders on your server, then send the resulting HTML (or a URL pointing to it) to the API. Calling the API With ScreenshotRun , a single curl request captures the rendered template as a PNG: curl -X POST "https://api.screenshotrun.com/v1/screenshot" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourblog.com/og-template?title=My+Post+Title", "viewport_width": 1200, "viewport_height": 630, "format": "png" }' The response gives you the image file. Save it to your CDN, set the og:image meta tag, and you're done. No browser to manage, no Chrome binary eating RAM on your CI server. Wiring It Into Your Build If you publish with a static sit
Most backend engineers already know how to handle HTTP errors. 400 means the request is bad. 401 means auth failed. 429 means rate limited. 500 means something broke upstream. Retry a few times, add exponential backoff, log the response body, move on. That works fine for many APIs. It works badly for LLM APIs. LLM providers may use normal HTTP status codes, but the operational meaning behind those errors is different enough that treating them like ordinary REST failures can make your app slower, more expensive, and harder to debug. The mistake I kept making Early on, I handled LLM failures the same way I handled every other external API: if ( response . status === 429 || response . status >= 500 ) { retryWithBackoff (); } Simple. Familiar. Dangerous. That logic misses the actual question your app needs to answer: What kind of LLM failure happened, and what should the product do next? Because an LLM API failure is rarely just "one HTTP request failed." It can break: a user-facing chat response a background agent run a document generation job a tool-calling workflow a batch evaluation pipeline a structured JSON generation step And each one needs different handling. Not all 429s mean the same thing For a normal API, 429 Too Many Requests usually means: Slow down and retry later. With LLM APIs, 429 can mean several different things. It might be a temporary rate limit: { "error" : { "message" : "Rate limit reached" , "type" : "rate_limit_error" } } Retrying with backoff may help here. But it might also mean quota exhaustion: { "error" : { "message" : "You exceeded your current quota" , "type" : "insufficient_quota" } } Retrying this does not help. It just adds latency, noisy logs, and a worse user experience. It could also be model-specific pressure. One model may be overloaded while another model from the same provider, or a different provider, would work fine. So your handler should distinguish between: temporary rate limit hard quota exhaustion model-level capacity is