<?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: MrAdex77</title>
    <description>The latest articles on DEV Community by MrAdex77 (@mradex77).</description>
    <link>https://dev.to/mradex77</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%2F556704%2Fd6491d6a-dcaf-41f9-8dc9-30094461c175.jpg</url>
      <title>DEV Community: MrAdex77</title>
      <link>https://dev.to/mradex77</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mradex77"/>
    <language>en</language>
    <item>
      <title>ASO for Developers: Get App Users Without Paying for Ads</title>
      <dc:creator>MrAdex77</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:42:59 +0000</pubDate>
      <link>https://dev.to/mradex77/aso-for-developers-get-app-users-without-paying-for-ads-2nbo</link>
      <guid>https://dev.to/mradex77/aso-for-developers-get-app-users-without-paying-for-ads-2nbo</guid>
      <description>&lt;p&gt;You can ship a solid app, pass review, and still watch the downloads counter sit in single digits. In December 2025 a developer posted on the Apple Developer Forums that their app had about 40 downloads two months after launching on both stores, even after a new icon and new screenshots (&lt;a href="https://developer.apple.com/forums/thread/811246" rel="noopener noreferrer"&gt;Apple Developer Forums&lt;/a&gt;). A reply in the same thread said Apple Ads hadn't produced a single download for them either.&lt;/p&gt;

&lt;p&gt;Before you buy installs, fix how the stores find your app. That's App Store Optimization (ASO), and every part of it can be done for free, a lot of it with a few lines of code. This post covers the playbook, two Node snippets you can run today, and the open-source ASO tool I maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Apple says 70% of App Store visitors use search to discover apps (2022 data). Your metadata decides whether you show up in those searches.&lt;/li&gt;
&lt;li&gt;The App Store indexes a 30-character name, a 30-character subtitle and a private 100-character keyword field. Google Play indexes a 30-character title, an 80-character short description and a 4,000-character description.&lt;/li&gt;
&lt;li&gt;New apps win long, specific phrases first. On 2026-10-17 the top US result for "habit tracker" in Apple's iTunes Search API had 148,007 ratings, while the #2 result for "habit tracker for couples" had 4.&lt;/li&gt;
&lt;li&gt;Track your rank every day down to position 200. A new app starts deep in the results, and a top-50 check shows nothing while it climbs.&lt;/li&gt;
&lt;li&gt;Buy ads after your listing ranks and converts. Apple Ads uses the same metadata to judge relevance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not start with paid ads?
&lt;/h2&gt;

&lt;p&gt;Paid installs cost money every time and stop when the budget does. SplitMetrics put the average Apple Ads cost per acquisition at $3.76 in its 2025 campaign data, $13.28 for finance apps and $19.74 for sports apps (&lt;a href="https://splitmetrics.com/blog/apple-search-ads-cost" rel="noopener noreferrer"&gt;SplitMetrics&lt;/a&gt;, September 2026). At the average, 1,000 installs cost $3,760, and the next 1,000 cost the same again.&lt;/p&gt;

&lt;p&gt;Ads also depend on the work ASO does. Apple's help pages say "your app title, descriptions, and keywords are all considerations Apple Ads uses to assess your app's relevance for specific search queries" (&lt;a href="https://ads.apple.com/app-store/help/apple-ads-basic/0045-improve-basic-app-promotion-results" rel="noopener noreferrer"&gt;Apple Ads Help&lt;/a&gt;). And if people tap your ad and bounce off a weak page, you paid for taps that never became installs.&lt;/p&gt;

&lt;p&gt;Ads do have a place. Apple reports tap-through install rates above 60% for ads at the top of search results (&lt;a href="https://ads.apple.com/app-store/" rel="noopener noreferrer"&gt;Apple Ads&lt;/a&gt;, November 2024 to October 2025). Once your listing ranks for a few phrases and converts, a small budget is a good way to test new keywords or push a launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is App Store Optimization?
&lt;/h2&gt;

&lt;p&gt;App Store Optimization is the practice of improving how an app ranks and converts in App Store and Google Play search. It covers which keywords you target, where you put them in your store metadata, how your listing compares with competing apps, and the ratings, icon and screenshots that turn a search result into an install.&lt;/p&gt;

&lt;p&gt;Search is the main way people find apps. Apple says 70% of App Store visitors use search to discover apps and almost 65% of downloads happen directly after a search (&lt;a href="https://ads.apple.com/app-store/" rel="noopener noreferrer"&gt;Apple Ads&lt;/a&gt;, figures from 2022). Apple's &lt;a href="https://www.apple.com/legal/app-store/transparency/2025/" rel="noopener noreferrer"&gt;2025 Transparency Report&lt;/a&gt; lists 2,172,472 apps on the App Store.&lt;/p&gt;

&lt;p&gt;Apple describes its ranking inputs as "text relevance (matches for your app's title, subtitle, keywords, and primary category), as well as user behavior (downloads, ratings and reviews, and more)" (&lt;a href="https://developer.apple.com/app-store/search/" rel="noopener noreferrer"&gt;App Store search&lt;/a&gt;). Google says Play ranks apps on "a combination of ratings, reviews, downloads, and other factors" (&lt;a href="https://support.google.com/googleplay/android-developer/answer/4448378" rel="noopener noreferrer"&gt;Play Console Help&lt;/a&gt;). Text relevance is the part you control on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which store fields count for search?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;App Store&lt;/th&gt;
&lt;th&gt;Google Play&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App name or title&lt;/td&gt;
&lt;td&gt;30 characters, indexed&lt;/td&gt;
&lt;td&gt;30 characters, indexed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subtitle&lt;/td&gt;
&lt;td&gt;30 characters, indexed&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword field&lt;/td&gt;
&lt;td&gt;100 characters, private, indexed&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short description&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;td&gt;80 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full description&lt;/td&gt;
&lt;td&gt;Not in Apple's list of search inputs&lt;/td&gt;
&lt;td&gt;Up to 4,000 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promotional text&lt;/td&gt;
&lt;td&gt;Apple says it doesn't affect ranking&lt;/td&gt;
&lt;td&gt;Doesn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: Apple's &lt;a href="https://developer.apple.com/app-store/product-page/" rel="noopener noreferrer"&gt;Creating your product page&lt;/a&gt;, &lt;a href="https://developer.apple.com/app-store/search/" rel="noopener noreferrer"&gt;App Store search&lt;/a&gt; and &lt;a href="https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information" rel="noopener noreferrer"&gt;Platform version information&lt;/a&gt;; Google's &lt;a href="https://support.google.com/googleplay/android-developer/answer/9859152" rel="noopener noreferrer"&gt;Create and set up your app&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rules for the App Store keyword field
&lt;/h3&gt;

&lt;p&gt;Apple's guidance for the field (&lt;a href="https://developer.apple.com/app-store/product-page/" rel="noopener noreferrer"&gt;Creating your product page&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Separate keywords with commas and no spaces.&lt;/li&gt;
&lt;li&gt;Don't repeat words from your app name, subtitle or category.&lt;/li&gt;
&lt;li&gt;Don't add plurals of words you already use in singular form.&lt;/li&gt;
&lt;li&gt;Leave out category names, filler words like "the" and the word "app".&lt;/li&gt;
&lt;li&gt;Leave out competitors' app names and trademarked terms. Apple lists them as reasons for rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a made-up but very typical field for an app named "Habit Tracker: Daily Goals" with the subtitle "Build routines that stick", run through AsoBeast's free &lt;a href="https://asobeast.com/tools/app-store-keyword-field-checker" rel="noopener noreferrer"&gt;keyword field checker&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before, 83 of 100 characters:
habit tracker, daily habits, the best app, goals, routine, routines, free, reminder

After, 8 of 100 characters:
reminder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Spaces after commas, filler words, words already in the name and subtitle, and plurals of words already used took up 75 characters. Use the space for phrases you don't rank for yet.&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%2Fcdx98ng944478dfh6avw.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%2Fcdx98ng944478dfh6avw.png" alt="The AsoBeast keyword field checker flagging five issues in a sample keyword field and freeing 75 characters." width="800" height="1126"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Rules for Google Play
&lt;/h3&gt;

&lt;p&gt;Google Play has no keyword field, so your phrases go into the title, short description and description. Google's advice is to "give an overview of your app or game using everyday language, not a list of keywords" (&lt;a href="https://support.google.com/googleplay/android-developer/answer/13393723" rel="noopener noreferrer"&gt;Best practices for your store listing&lt;/a&gt;). Its metadata policy says to "avoid using repetitive or unrelated keywords or references" and bans ranking claims such as "#1" in the title, icon or developer name (&lt;a href="https://support.google.com/googleplay/android-developer/answer/9898842" rel="noopener noreferrer"&gt;Metadata policy&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you find keywords a new app can win?
&lt;/h2&gt;

&lt;p&gt;Collect 30 to 50 candidate phrases, then keep the ones that real people search where the current top results look beatable.&lt;/p&gt;

&lt;p&gt;Store autocomplete is the cheapest source of real search phrases. On Google Play you can read it with &lt;a href="https://www.npmjs.com/package/@mradex77/google-play-scraper" rel="noopener noreferrer"&gt;@mradex77/google-play-scraper&lt;/a&gt;, the scraper from my &lt;a href="https://blog.stackademic.com/how-to-scrape-google-play-store-with-node-js-in-2026-complete-tutorial-6ef7d4546b9c" rel="noopener noreferrer"&gt;Stackademic post on scraping Google Play&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;suggest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;habit tracker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;habit tracker for&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suggestions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;suggest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output on 2026-10-17:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;habit tracker: habit tracker, habit tracker free, habit tracker app, habit tracker widget, habit tracker free app
habit tracker for: habit tracker for adhd, habit tracker for couples, habit tracker for free, habit tracker for study, habit tracker for students
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Ending a seed with "for" or another preposition pulls out the audience-specific phrases. Check those next, because the apps ranking for them can be small. I looked up the top App Store results in the US for two of these phrases through Apple's iTunes Search API on the same day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"habit tracker": the #1 app had 148,007 ratings, and the #4 had 764,669.&lt;/li&gt;
&lt;li&gt;"habit tracker for couples": the #2 app had 4 ratings, and the #4 had none.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apple gives the same advice in its own search guide. Popular terms "are highly competitive", especially for newer or smaller apps, while less common terms bring less traffic and less competition (&lt;a href="https://developer.apple.com/app-store/search/" rel="noopener noreferrer"&gt;App Store search&lt;/a&gt;). Start where an app with 4 ratings can rank, and move to the head terms once your ratings and downloads grow.&lt;/p&gt;

&lt;p&gt;Two more sources are worth a pass: the names and subtitles of the top 10 apps for your main phrase, and reviews, yours and your competitors'. People describe an app in the words they search with.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you check your App Store rank in code?
&lt;/h2&gt;

&lt;p&gt;Apple's public &lt;a href="https://performance-partners.apple.com/search-api" rel="noopener noreferrer"&gt;iTunes Search API&lt;/a&gt; returns up to 200 apps for a search term, which is enough for a rank check. This needs Node 22 or newer and no dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://itunes.apple.com/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;software&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Not in the top &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node rank.mjs 570060128 "learn spanish" us
#1 for "learn spanish" in US
$ node rank.mjs 570060128 "spanish vocabulary" de
#10 for "spanish vocabulary" in DE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The app id is the number after &lt;code&gt;id&lt;/code&gt; in your App Store URL (570060128 is Duolingo). Keep the request rate low: Apple documents the Search API limit as "approximately 20 calls per minute (subject to change)", which is why AsoBeast's free rank tracker checks at most 10 keywords at a time. The API returns iPhone apps, and a search on your own phone can differ because Apple Ads results and your history change what you see.&lt;/p&gt;

&lt;p&gt;Run it every day and store the result, and check all 200 positions. A new app usually first shows up far down the list. With a top-50 check you'd see "not ranked" while the app climbs from 180 to 60, and conclude that nothing worked.&lt;/p&gt;

&lt;p&gt;If you'd rather not write a cron job, AsoBeast's &lt;a href="https://asobeast.com/tools/app-store-keyword-rank-tracker" rel="noopener noreferrer"&gt;free App Store keyword rank tracker&lt;/a&gt; does the same check in your browser for up to 10 keywords in any of 175 countries, with no signup.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you research competitors?
&lt;/h2&gt;

&lt;p&gt;Your competitors are whoever holds the top 10 for your target phrases, which may not be the apps you had in mind. For each, note the name and subtitle, the rating count and the date of the last update. Phrases where they rank and you don't are gaps. Phrases where nobody strong ranks are openings.&lt;/p&gt;

&lt;p&gt;Keep watching them. A competitor that rewrites its title or subtitle may be going after a new keyword, and you can follow its rank on that phrase for a few weeks without spending one of your own releases on the experiment. Their one- and two-star reviews tell you what to promise in your screenshots.&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%2Fufvwolae191ywgoy6lar.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%2Fufvwolae191ywgoy6lar.png" alt="AsoBeast's metadata change timeline showing a competitor's title change with the before and after values." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How should you ship and measure ASO changes?
&lt;/h2&gt;

&lt;p&gt;Treat the first update as the baseline. Clean up the keyword field and put your main phrase in the name, subtitle or title in one release, because wasted characters cost you every day they stay. After that, change one field per release and wait about two weeks before you judge it. Change three fields at once and you won't know which one moved your rankings.&lt;/p&gt;

&lt;p&gt;On the App Store, the keyword field sits on your version page in App Store Connect, so new keywords ship with a version you submit for review. On Google Play, listing edits can go through review too.&lt;/p&gt;

&lt;p&gt;Track three numbers: your daily rank for each target phrase, installs from search, and the conversion rate from page view to install. In App Store Connect, App Analytics splits downloads by source, and App Store Search is one of them.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where do keyword installs show up in Google Play Console?
&lt;/h3&gt;

&lt;p&gt;Under "Google Play explore". Play Console's "Google Play search" source counts only people who searched for your app's name or a closely associated brand. Generic searches like "racing game" and taps on autocomplete suggestions count as explore, and the Search term dimension shows what people typed (&lt;a href="https://support.google.com/googleplay/android-developer/answer/9859173" rel="noopener noreferrer"&gt;Play Console Help&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you turn rankings into installs?
&lt;/h2&gt;

&lt;p&gt;Once you rank, the icon, the first screenshots and the rating decide whether people install. Both stores give you the tools for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a rating after the user has done something that worked. On iOS, &lt;code&gt;SKStoreReviewController&lt;/code&gt; shows Apple's prompt, up to three times in a 365-day period (&lt;a href="https://developer.apple.com/app-store/ratings-and-reviews/" rel="noopener noreferrer"&gt;Ratings, reviews, and responses&lt;/a&gt;). On Android, use the &lt;a href="https://developer.android.com/guide/playcore/in-app-review" rel="noopener noreferrer"&gt;Google Play In-App Review API&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Test the page. Apple's &lt;a href="https://developer.apple.com/app-store/product-page-optimization/" rel="noopener noreferrer"&gt;product page optimization&lt;/a&gt; runs up to three alternate versions of your icon, screenshots and app previews against the original. Google Play's &lt;a href="https://support.google.com/googleplay/android-developer/answer/6227309" rel="noopener noreferrer"&gt;store listing experiments&lt;/a&gt; test the icon, feature graphic and screenshots, and localized experiments can test descriptions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  AsoBeast: the open-source ASO tool I maintain
&lt;/h2&gt;

&lt;p&gt;The snippets above are fine for ten keywords in one country. AsoBeast does it daily for hundreds of keywords across countries, in both stores, and keeps the history. It's an App Store Optimization tool for the App Store and Google Play: it tracks keyword rankings daily, watches competitors and reviews, and turns it all into a to-do list ranked by impact. I wanted that workflow without paying per keyword or sending my keyword list to a vendor, so the whole thing is AGPL-3.0 with no open core.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AsoBeast" rel="noopener noreferrer"&gt;
        AsoBeast
      &lt;/a&gt; / &lt;a href="https://github.com/AsoBeast/asobeast" rel="noopener noreferrer"&gt;
        asobeast
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open source, self-hosted App Store Optimization toolkit for the Apple App Store and Google Play.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
  &lt;a href="https://asobeast.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FAsoBeast%2Fasobeast%2FHEAD%2Fdocs%2Fimages%2Flogo-mark.png" alt="AsoBeast" width="112"&gt;&lt;/a&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AsoBeast&lt;/h1&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;&lt;a href="https://asobeast.com" rel="nofollow noopener noreferrer"&gt;AsoBeast&lt;/a&gt; is an App Store Optimization (ASO) tool and keyword rank tracker for the Apple App Store and Google Play.&lt;/strong&gt;&lt;/p&gt;
&lt;br&gt;
  &lt;p&gt;Use the hosted service at &lt;a href="https://asobeast.com" rel="nofollow noopener noreferrer"&gt;asobeast.com&lt;/a&gt;, or self-host the open source edition on your own server.&lt;/p&gt;
&lt;br&gt;
  &lt;p&gt;&lt;br&gt;
    &lt;a href="https://github.com/AsoBeast/asobeast/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="License: AGPL-3.0" src="https://camo.githubusercontent.com/0fb4f7634808706feb5d9bf669e078e2da524534575a39f2f08c74de3d8a1135/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4147504c2d2d332e302d626c75652e737667"&gt;&lt;/a&gt;&lt;br&gt;
    &lt;a href="https://github.com/AsoBeast/asobeast/releases" rel="noopener noreferrer"&gt;&lt;img alt="Release" src="https://camo.githubusercontent.com/7e327b0aaa2b06437b1cf5094c704365b72c56b2468124edef28115c16276334/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f41736f42656173742f61736f6265617374"&gt;&lt;/a&gt;&lt;br&gt;
    &lt;a href="https://docs.asobeast.com" rel="nofollow noopener noreferrer"&gt;&lt;img alt="Documentation" src="https://camo.githubusercontent.com/a2bff85180c3e23b048faa8e7d759d1a43179159cfe2fa0c1172573d8a4cfe37/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d646f63732e61736f62656173742e636f6d2d6f72616e6765"&gt;&lt;/a&gt;&lt;br&gt;
    &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/847c4aa1a1f499587018732e9c6429fcd745a19a9e910327914909c9b9fd353b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73746f7265732d41707025323053746f7265253230253242253230476f6f676c65253230506c61792d6c6967687467726579"&gt;&lt;img alt="Stores" src="https://camo.githubusercontent.com/847c4aa1a1f499587018732e9c6429fcd745a19a9e910327914909c9b9fd353b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f73746f7265732d41707025323053746f7265253230253242253230476f6f676c65253230506c61792d6c6967687467726579"&gt;&lt;/a&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;br&gt;
  &lt;p&gt;&lt;br&gt;
    &lt;a href="https://asobeast.com" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Website&lt;/strong&gt;&lt;/a&gt; ·&lt;br&gt;
    &lt;a href="https://docs.asobeast.com/quickstart" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Quickstart&lt;/strong&gt;&lt;/a&gt; ·&lt;br&gt;
    &lt;a href="https://docs.asobeast.com/install/docker-compose" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;Self-host&lt;/strong&gt;&lt;/a&gt; ·&lt;br&gt;
    &lt;a href="https://docs.asobeast.com/mcp/introduction" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;MCP server&lt;/strong&gt;&lt;/a&gt; ·&lt;br&gt;
    &lt;a href="https://docs.asobeast.com/api-reference/introduction" rel="nofollow noopener noreferrer"&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/a&gt; ·&lt;br&gt;
    &lt;a href="https://github.com/AsoBeast/asobeast/CHANGELOG.md" rel="noopener noreferrer"&gt;&lt;strong&gt;Changelog&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
  &lt;/p&gt;
&lt;br&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is AsoBeast?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;AsoBeast is an App Store Optimization (ASO) tool and keyword rank tracker for the Apple App Store and Google Play. It imports a listing, tracks keyword rankings daily to a depth of 200, watches competitors, reviews and metadata, and turns that history into a prioritized queue of ASO work. This repository is the free, open source edition that you run on your own server, and the hosted service at &lt;a href="https://asobeast.com" rel="nofollow noopener noreferrer"&gt;asobeast.com&lt;/a&gt; runs the same code.&lt;/p&gt;

&lt;p&gt;Every store request runs on the machine hosting AsoBeast as an ordinary public search or page request, so Apple and Google see each…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AsoBeast/asobeast" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;What it does in v1.11.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Imports an App Store or Google Play link and checks its keywords daily to position 200, in any of 175 App Store storefronts and 225 Google Play countries. One search per keyword covers your app and all of its competitors.&lt;/li&gt;
&lt;li&gt;Scores keyword popularity and difficulty, shows where each number comes from, and suggests keywords from your metadata, search results, similar apps, competitors and reviews.&lt;/li&gt;
&lt;li&gt;Finds competitors in live search results, lists keyword gaps and records the title, subtitle and description changes it detects.&lt;/li&gt;
&lt;li&gt;Syncs reviews and rating history and groups reviews into themes.&lt;/li&gt;
&lt;li&gt;Runs an Action Center: fifteen deterministic rules read the stored history and produce items like "Push 'make memes' from #12 into the top 10", each with its evidence and a check that the fix worked. AI is optional and never invents or reorders an action.&lt;/li&gt;
&lt;li&gt;Ships a REST API and an MCP server that reads by default, so Claude Code, Claude Desktop or Cursor can query your rankings.&lt;/li&gt;
&lt;/ul&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%2Fzjbu8qmgcvzjxszip10y.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%2Fzjbu8qmgcvzjxszip10y.png" alt="The AsoBeast dashboard with an Action Center list, keyword climbers and fallers, app visibility charts and recent competitor changes." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stack is TypeScript throughout: NestJS, Prisma, PostgreSQL 18, BullMQ, Redis 8 and Next.js, in a pnpm and Turborepo monorepo. The Google Play side runs on the same &lt;code&gt;@mradex77/google-play-scraper&lt;/code&gt; as the snippet above. There's no telemetry, and apart from the store searches themselves, nothing leaves your server unless you turn on an integration such as webhook alerts or email.&lt;/p&gt;

&lt;p&gt;To run it, you need Docker with Compose and roughly 2.5 GB of memory. This uses the published images, with no clone or build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/AsoBeast/asobeast/main/docker-compose.pull.yml
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'POSTGRES_PASSWORD=%s\nAUTH_SECRET=%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.pull.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3001&lt;/code&gt;, create the owner account (registration closes after that), and import a store link. Tracking starts right away. The &lt;a href="https://docs.asobeast.com/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers the rest, including a one-click Railway deploy. There's also a hosted version at &lt;a href="https://asobeast.com" rel="noopener noreferrer"&gt;asobeast.com&lt;/a&gt; that runs the same code, if you'd rather not run a server.&lt;/p&gt;

&lt;p&gt;It has limits, and the README lists them. Popularity and difficulty are estimates built from public store data. The stores' rate limits, not your hardware, cap how many keyword markets one instance can track. Backups and upgrades are your job when you self-host.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is ASO free?
&lt;/h3&gt;

&lt;p&gt;The work is. App Store Connect and Google Play Console analytics and listing tests are free, the iTunes Search API and store autocomplete are public, and open-source tools like AsoBeast are free to self-host. Paid ASO tools save time once you track hundreds of keywords in many countries.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does ASO take to work?
&lt;/h3&gt;

&lt;p&gt;A metadata change goes live with your next approved App Store version, or once Google reviews your listing update. Rankings move over the following weeks. Give each change about two weeks before you judge it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I run Apple Ads at launch?
&lt;/h3&gt;

&lt;p&gt;Only once the listing converts. Apple judges ad relevance from your title, description and keywords, so ASO comes first either way. After that, ads are a good way to test a phrase before you put it in your title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first hour
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;suggest()&lt;/code&gt; on your main phrase with "for", "with" and "without" appended, and write down 30 phrases.&lt;/li&gt;
&lt;li&gt;Check your rank for the ten most relevant ones with the snippet or the &lt;a href="https://asobeast.com/tools/app-store-keyword-rank-tracker" rel="noopener noreferrer"&gt;free rank tracker&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Paste your keyword field into the &lt;a href="https://asobeast.com/tools/app-store-keyword-field-checker" rel="noopener noreferrer"&gt;keyword field checker&lt;/a&gt; and fill the characters it frees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If AsoBeast saves you an ad budget, a star on &lt;a href="https://github.com/AsoBeast/asobeast" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps other developers find it, and issues and pull requests are welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>There's No Google Play API. Here's How I Scrape the Play Store in Node.js (2026)</title>
      <dc:creator>MrAdex77</dc:creator>
      <pubDate>Thu, 09 Jul 2026 10:04:24 +0000</pubDate>
      <link>https://dev.to/mradex77/theres-no-google-play-api-heres-how-i-scrape-the-play-store-in-nodejs-2026-43hi</link>
      <guid>https://dev.to/mradex77/theres-no-google-play-api-heres-how-i-scrape-the-play-store-in-nodejs-2026-43hi</guid>
      <description>&lt;p&gt;Ask Google for an API to read Play Store listings, search results, or reviews. The answer is the same in 2026 as it was a decade ago: there isn't one. The Google Play Developer API only manages apps &lt;em&gt;you&lt;/em&gt; publish. Reading anyone else's public store data has no official endpoint at all.&lt;/p&gt;

&lt;p&gt;That's a strange hole for a store this size. Google Play lists about 1.9 million apps (&lt;a href="https://www.appbrain.com/stats/number-of-android-apps" rel="noopener noreferrer"&gt;AppBrain&lt;/a&gt;, 2026) and serves 2.5 billion+ monthly users across 190+ countries. It made $49.2 billion in 2025 (&lt;a href="https://www.businessofapps.com/data/google-play-statistics/" rel="noopener noreferrer"&gt;Business of Apps, Google Play Statistics&lt;/a&gt;, 2026). Every ASO tool, market report, and research paper built on that data got it one way: scraping.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/MrAdex77/google-play-scraper" rel="noopener noreferrer"&gt;&lt;code&gt;@mradex77/google-play-scraper&lt;/code&gt;&lt;/a&gt;, an open-source, strict-TypeScript Google Play scraper for Node.js. This post is the full working guide: install, app details, search, top charts, paginated reviews, and not getting rate limited. Every snippet comes from the repo's &lt;a href="https://github.com/MrAdex77/google-play-scraper/tree/main/examples" rel="noopener noreferrer"&gt;runnable examples&lt;/a&gt;, so you can clone and reproduce all of it.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/MrAdex77" rel="noopener noreferrer"&gt;
        MrAdex77
      &lt;/a&gt; / &lt;a href="https://github.com/MrAdex77/google-play-scraper" rel="noopener noreferrer"&gt;
        google-play-scraper
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Google Play scraper for Node.js with a fully typed TypeScript API. App details, search, top charts, reviews, permissions and data safety. A modern replacement for the unmaintained google-play-scraper.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;google-play-scraper&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@mradex77/google-play-scraper" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4764ff829252118dfe62380a4bc376220c8a3af2296abb92f71510c144767852/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f406d726164657837372f676f6f676c652d706c61792d736372617065722e737667" alt="npm version"&gt;&lt;/a&gt;
&lt;a href="https://www.npmjs.com/package/@mradex77/google-play-scraper" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5627f0652dfaefcc830c428622fad71e263dfd546049a889670772fbeea37684/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f646d2f406d726164657837372f676f6f676c652d706c61792d736372617065722e737667" alt="npm downloads"&gt;&lt;/a&gt;
&lt;a href="https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/MrAdex77/google-play-scraper/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/MrAdex77/google-play-scraper/actions/workflows/e2e.yml/badge.svg" alt="Live contract tests"&gt;&lt;/a&gt;
&lt;a href="https://www.npmjs.com/package/@mradex77/google-play-scraper" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e8d63107520ca3c7e9d32ce98c33c26b528c7c4254f291049660881c0fc85124/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f74797065732f406d726164657837372f676f6f676c652d706c61792d736372617065722e737667" alt="types"&gt;&lt;/a&gt;
&lt;a href="https://nodejs.org" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c72396c786236dc21f1f31baca5343a0a06ef9f982f662fda609cca83ab981bf/68747470733a2f2f696d672e736869656c64732e696f2f6e6f64652f762f406d726164657837372f676f6f676c652d706c61792d736372617065722e737667" alt="node"&gt;&lt;/a&gt;
&lt;a href="https://github.com/MrAdex77/google-play-scraper/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8f8266b09c52153a643ac3fb18eb061918bd3038e386064d60527c89b434ac8b/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f406d726164657837372f676f6f676c652d706c61792d736372617065722e737667" alt="license"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scrape Google Play app data in Node.js with a fully typed TypeScript API.&lt;/strong&gt; Fetch app details, search results, top charts, developer pages, similar apps, user reviews, permissions and data safety information directly from the Play Store. Built for ASO research, app market analysis, competitor tracking, review monitoring and data pipelines.&lt;/p&gt;
&lt;p&gt;This is a modern TypeScript rewrite of the popular but unmaintained &lt;a href="https://github.com/facundoolano/google-play-scraper" rel="noopener noreferrer"&gt;&lt;code&gt;google-play-scraper&lt;/code&gt;&lt;/a&gt; package. The public method names, options and constants match the original, so migrating is usually just a matter of swapping the import.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this library&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully typed results.&lt;/strong&gt; Every method returns a precise TypeScript type derived from a &lt;a href="https://zod.dev" rel="nofollow noopener noreferrer"&gt;zod&lt;/a&gt; schema that validates the scraped data at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No HTTP dependency.&lt;/strong&gt; Runs on the native &lt;code&gt;fetch&lt;/code&gt; of Node.js 22 and newer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilient by design.&lt;/strong&gt; Every fragile Google Play array path lives behind a spec layer with ordered fallback paths, so a single moved index does not take a…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/MrAdex77/google-play-scraper" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No official Google Play Store API exists for listings, search, or reviews; scraping public pages is how every app-intelligence dataset gets built.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@mradex77/google-play-scraper&lt;/code&gt; rewrites the unmaintained classic in strict TypeScript, validates everything with zod at runtime, and tests against live Google Play daily in CI.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm install&lt;/code&gt;, one import, &lt;code&gt;await app({ appId })&lt;/code&gt;: 55 typed fields per app.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Makes Google Play So Hard to Scrape?
&lt;/h2&gt;

&lt;p&gt;Not the fetching. The parsing. Google Play ships its data as enormous, deeply nested JavaScript arrays with zero labels, and the array positions shift a few times a year. Your scraper doesn't break because Google blocks you. It breaks because &lt;code&gt;data[1][2][72][0][1]&lt;/code&gt; quietly became &lt;code&gt;data[1][2][74][0][1]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The monetization layout raises the stakes. In 2026, roughly 97% of Play Store listings are free (&lt;a href="https://42matters.com/google-play-statistics-and-trends" rel="noopener noreferrer"&gt;42matters, Google Play Statistics and Trends&lt;/a&gt;, 2026). That makes reviews, install counts, and IAP flags the analytically interesting fields. Those live deepest in the unlabeled arrays. Android's ~72% global OS share (&lt;a href="https://gs.statcounter.com/os-market-share/mobile/worldwide" rel="noopener noreferrer"&gt;StatCounter Global Stats&lt;/a&gt;, 2026) means that data describes most of the world's mobile users.&lt;/p&gt;

&lt;p&gt;So the real engineering question isn't "how do I scrape Google Play?" It's "how do I keep a Google Play scraper alive?" Everything below is designed around that question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use the Original google-play-scraper?
&lt;/h2&gt;

&lt;p&gt;Because nobody's maintaining it. &lt;a href="https://github.com/facundoolano/google-play-scraper" rel="noopener noreferrer"&gt;facundoolano/google-play-scraper&lt;/a&gt; defined this space and still moves about 94,000 npm downloads a week as of July 2026 (&lt;a href="https://api.npmjs.org/downloads/point/last-week/google-play-scraper" rel="noopener noreferrer"&gt;npm registry download counts&lt;/a&gt;, 2026). But when Google shuffles an index now, the fix has to come from a fork or a monkey-patch in your own code.&lt;/p&gt;

&lt;p&gt;My rewrite keeps the exact public surface — same method names, same options, same constants — and rebuilds the internals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;
&lt;a class="mentioned-user" href="https://dev.to/mradex77"&gt;@mradex77&lt;/a&gt;/google-play-scraper&lt;/th&gt;
&lt;th&gt;Original google-play-scraper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript, strict mode&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type definitions&lt;/td&gt;
&lt;td&gt;Generated from zod schemas&lt;/td&gt;
&lt;td&gt;Community typings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime validation&lt;/td&gt;
&lt;td&gt;zod on every input and output&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;Typed error classes&lt;/td&gt;
&lt;td&gt;Plain &lt;code&gt;Error&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Module formats&lt;/td&gt;
&lt;td&gt;ESM + CommonJS + &lt;code&gt;.d.ts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ESM only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breakage detection&lt;/td&gt;
&lt;td&gt;Daily live contract tests in CI&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;td&gt;Unmaintained&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two internals that matter most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spec layer with fallback paths.&lt;/strong&gt; Every fragile array index lives in a per-feature &lt;code&gt;specs.ts&lt;/code&gt; file as a typed constant with ordered fallback candidates. A moved index degrades one field, and extraction throws a single &lt;code&gt;SpecError&lt;/code&gt; listing every broken field plus every path it tried. Nothing silently returns garbage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily live contract tests.&lt;/strong&gt; CI runs the e2e suite against real &lt;code&gt;play.google.com&lt;/code&gt; every day and auto-opens a labeled GitHub issue on failure. The maintainer finds out before your pipeline does.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Node.js 22.12+, native &lt;code&gt;fetch&lt;/code&gt;, no HTTP dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mradex77/google-play-scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named exports or an aggregate default export (the default export is drop-in compatible with the original package):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;gplay&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.google.android.apps.translate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CommonJS also works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;gplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;panda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API keys, no headless browser, no Python sidecar.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Fetch App Details?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app()&lt;/code&gt; takes an &lt;code&gt;appId&lt;/code&gt; and returns 55 typed fields: title, description, rating histogram, installs, price, screenshots, developer info, and the rest. The &lt;code&gt;appId&lt;/code&gt; is sitting in every Play Store URL as the &lt;code&gt;?id=&lt;/code&gt; parameter:&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%2F20ryvoy0q7ol4lt09xju.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%2F20ryvoy0q7ol4lt09xju.png" alt="Annotated Google Play store URL showing that the id query parameter is the appId used by the scraper" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adapted from &lt;a href="https://github.com/MrAdex77/google-play-scraper/blob/main/examples/all-methods.ts" rel="noopener noreferrer"&gt;&lt;code&gt;examples/all-methods.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Title:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Developer:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; / 5 (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; reviews)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Installs:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;installs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Price:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;free&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Genre:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Updated:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toDateString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the repo's example script hitting live Google Play while I wrote this post:&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%2Fdn88wpp3e0u16cr93k9j.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%2Fdn88wpp3e0u16cr93k9j.png" alt="Terminal output of pnpm example from the google-play-scraper repository showing live app details and search results scraped from Google Play" width="799" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The zod layer is what makes this pleasant in a TypeScript codebase. &lt;code&gt;details&lt;/code&gt; autocompletes all 55 fields, and if Google ships something malformed, you get a validation error at the boundary instead of &lt;code&gt;undefined&lt;/code&gt; surfacing three layers into your pipeline.&lt;/p&gt;

&lt;p&gt;Different market? Every method takes &lt;code&gt;lang&lt;/code&gt; and &lt;code&gt;country&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;german&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Do You Search the Play Store Programmatically?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;search()&lt;/code&gt; returns up to 250 results per query. Roughly 2,400 new apps land on Google Play every day in 2026 (&lt;a href="https://www.businessofapps.com/data/google-play-statistics/" rel="noopener noreferrer"&gt;Business of Apps&lt;/a&gt;, 2026), so keyword rankings rot fast. Automating this is the only way to keep up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;suggest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;where am i geography quiz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results carry title, &lt;code&gt;appId&lt;/code&gt;, icon, developer, price, and score; &lt;code&gt;fullDetail: true&lt;/code&gt; upgrades each to the full 55-field record. There's also &lt;code&gt;suggest()&lt;/code&gt;, which exposes Play's own autocomplete. That's free ASO keyword research:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suggestions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;suggest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;where am&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suggestions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Do You Pull Top Charts and Competitors?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;list()&lt;/code&gt; fetches the ranked collections (top free, top paid, top grossing) per category:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;topGames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TOP_FREE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;topGames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constants are exported, frozen, and typed (&lt;code&gt;collection.TOP_FREE&lt;/code&gt;, &lt;code&gt;category.GAME_PUZZLE&lt;/code&gt;, &lt;code&gt;category.FAMILY&lt;/code&gt;, …), so the editor autocompletes what other libraries make you copy from a README.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;developer()&lt;/code&gt; returns a publisher's entire portfolio. &lt;code&gt;similar()&lt;/code&gt; returns the "similar apps" rail. That's effectively Google naming your competitor set for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;similar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;portfolio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;devId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5700313618786177705&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;competitors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Do You Scrape Reviews (All of Them)?
&lt;/h2&gt;

&lt;p&gt;Reviews hide behind Google's internal &lt;code&gt;batchexecute&lt;/code&gt; endpoint with token-based pagination — the single most annoying thing to reverse-engineer by hand. &lt;code&gt;reviews()&lt;/code&gt; wraps it, and every response is a &lt;code&gt;{ data, nextPaginationToken }&lt;/code&gt; envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sort&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEWEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;firstPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;★ by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPaginationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nextPaginationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPaginationToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loop until the token comes back undefined and you've walked every review: score, text, ISO 8601 date, thumbs-up count, app version, developer reply.&lt;/p&gt;

&lt;p&gt;Bonus pair almost no scraper exposes — &lt;code&gt;permissions()&lt;/code&gt; and &lt;code&gt;datasafety()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;datasafety&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safety&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;datasafety&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Permissions:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;perms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Collected data types:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;safety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectedData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Privacy policy:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;safety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privacyPolicyUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the full permission list plus the parsed Data Safety panel: collected data, shared data, declared security practices. Useful far beyond ASO — privacy research runs on exactly these fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Stay Under the Rate Limits?
&lt;/h2&gt;

&lt;p&gt;Three levers, all built in. Use all three for anything bigger than a handful of requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;throttle&lt;/code&gt;&lt;/strong&gt; caps requests per second; exponential backoff with &lt;code&gt;Retry-After&lt;/code&gt; support is automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;memoized()&lt;/code&gt;&lt;/strong&gt; wraps every method in an in-memory LRU cache, so repeated calls inside the TTL never touch Google. In the repo's example run the second call returns in a fraction of a millisecond:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;memoized&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memoized&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxAgeMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.adex77.WhereAmI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Typed errors&lt;/strong&gt; let your pipeline branch on the cause instead of regex-matching messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SpecError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.does.not.exist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;NotFoundError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No such app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Backing off — Google is rate limiting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;SpecError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Google changed its layout:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SpecError.failures&lt;/code&gt; is the underrated one. It names every field that stopped resolving and every path that was tried — a complete, machine-generated repair ticket for the next layout change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is It Legal?
&lt;/h2&gt;

&lt;p&gt;The library reads only what any browser can: public pages. Scraping still lives in a gray zone, so the Google Play Terms of Service and your local laws are on you, particularly for the personal data inside reviews. My working rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throttle everything; spread big jobs over time&lt;/li&gt;
&lt;li&gt;Cache hard; never re-fetch what you already hold&lt;/li&gt;
&lt;li&gt;Analyze, don't republish Google's content wholesale&lt;/li&gt;
&lt;li&gt;Treat reviewer names and avatars as personal data under GDPR-style regimes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not affiliated with or endorsed by Google. Scrape responsibly.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is there any official API for Google Play Store data?
&lt;/h3&gt;

&lt;p&gt;No. Google's APIs only cover apps you publish yourself. For reading public listings, search results, top charts, or reviews of arbitrary apps, no official API exists in 2026. That's why open-source scrapers parsing play.google.com are the standard approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I scrape Google Play reviews in Node.js?
&lt;/h3&gt;

&lt;p&gt;Install &lt;code&gt;@mradex77/google-play-scraper&lt;/code&gt;, then call &lt;code&gt;reviews()&lt;/code&gt; with the target &lt;code&gt;appId&lt;/code&gt; and &lt;code&gt;paginate: true&lt;/code&gt;. Keep feeding the returned &lt;code&gt;nextPaginationToken&lt;/code&gt; back until it's undefined. Add &lt;code&gt;throttle&lt;/code&gt; so apps with millions of reviews don't trip Google's rate limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  What data can a Google Play scraper get?
&lt;/h3&gt;

&lt;p&gt;App details (55 fields including installs, ratings histogram, and screenshots), search results, autocomplete suggestions, top charts by category, developer portfolios, similar apps, paginated reviews, permissions, and the parsed Data Safety section. That coverage spans the ~1.9 million apps live on Google Play (&lt;a href="https://www.appbrain.com/stats/number-of-android-apps" rel="noopener noreferrer"&gt;AppBrain&lt;/a&gt;, 2026).&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I migrate from the original google-play-scraper package?
&lt;/h3&gt;

&lt;p&gt;Yes, usually by swapping one import. Method names, options, and constants match. You gain strict TypeScript types from zod schemas, runtime validation, typed error classes, dual ESM/CommonJS builds, and daily live contract tests that the unmaintained original lacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why can't I run this in the browser?
&lt;/h3&gt;

&lt;p&gt;Google Play's pages don't send CORS headers permitting cross-origin reads, so no browser-based scraper can work. The library targets Node.js 22.12+ and uses its native &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;Google Play scraping in 2026 in three sentences. There's no official API and there won't be one soon. The data sits in unlabeled arrays that shift under you. Pick a tool that treats breakage as a first-class design problem, or become the maintainer yourself at 2 a.m.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mradex77/google-play-scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;gplay&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mradex77/google-play-scraper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;com.google.android.apps.translate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/MrAdex77/google-play-scraper#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; documents every method and return shape, and &lt;a href="https://github.com/MrAdex77/google-play-scraper/tree/main/examples" rel="noopener noreferrer"&gt;examples/all-methods.ts&lt;/a&gt; runs the entire API end to end.&lt;/p&gt;

&lt;p&gt;If this saved you from reverse-engineering &lt;code&gt;batchexecute&lt;/code&gt;, a star on &lt;a href="https://github.com/MrAdex77/google-play-scraper" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; genuinely helps the project. And if a field ever stops resolving, open an issue with the &lt;code&gt;SpecError&lt;/code&gt; output. Fastest bug report format there is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sources: &lt;a href="https://www.businessofapps.com/data/google-play-statistics/" rel="noopener noreferrer"&gt;Business of Apps, Google Play Statistics&lt;/a&gt;, retrieved 2026-07-08. &lt;a href="https://www.appbrain.com/stats/number-of-android-apps" rel="noopener noreferrer"&gt;AppBrain, Number of Android apps on Google Play&lt;/a&gt;, retrieved 2026-07-08. &lt;a href="https://gs.statcounter.com/os-market-share/mobile/worldwide" rel="noopener noreferrer"&gt;StatCounter Global Stats, Mobile OS Market Share Worldwide&lt;/a&gt;, retrieved 2026-07-08. &lt;a href="https://42matters.com/google-play-statistics-and-trends" rel="noopener noreferrer"&gt;42matters, Google Play Statistics and Trends&lt;/a&gt;, retrieved 2026-07-08. &lt;a href="https://api.npmjs.org/downloads/point/last-week/google-play-scraper" rel="noopener noreferrer"&gt;npm registry, weekly download counts for google-play-scraper&lt;/a&gt;, retrieved 2026-07-08.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building "Where Am I?": A GeoGuessr Alternative for Mobile</title>
      <dc:creator>MrAdex77</dc:creator>
      <pubDate>Wed, 08 Jan 2025 18:34:46 +0000</pubDate>
      <link>https://dev.to/mradex77/building-where-am-i-a-geoguessr-alternative-for-mobile-5gbj</link>
      <guid>https://dev.to/mradex77/building-where-am-i-a-geoguessr-alternative-for-mobile-5gbj</guid>
      <description>&lt;p&gt;Hi Dev Community! 👋&lt;/p&gt;

&lt;p&gt;I’m thrilled to share with you the journey of building &lt;em&gt;Where Am I?&lt;/em&gt;, a mobile app designed as a free alternative to GeoGuessr and CityGuessr. It’s a location-guessing game that immerses players in street walk videos from around the globe. As a solo developer, this project has been a labor of love, and I wanted to give you a behind-the-scenes look at how it all came together from a tech perspective.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;The inspiration for &lt;em&gt;Where Am I?&lt;/em&gt; started during the COVID-19 pandemic. With everyone stuck indoors, I wanted to create a game that offered a sense of exploration and adventure. Initially, the app was a simple prototype with basic features. It gained traction with over &lt;strong&gt;50,000 downloads&lt;/strong&gt; and amazing user feedback. Encouraged by the positive response, I decided to rewrite the app from scratch, making it more robust, feature-rich, and scalable.&lt;/p&gt;




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

&lt;p&gt;To bring this project to life, I chose the following technologies:&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend: React Native
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Where Am I?&lt;/em&gt; is a cross-platform mobile app built with &lt;strong&gt;React Native&lt;/strong&gt;. This allowed me to deliver a seamless experience for both Android and iOS users using a single codebase. The frontend was developed with a combination of &lt;strong&gt;JavaScript&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt; to ensure type safety and scalability.&lt;/p&gt;

&lt;p&gt;Key libraries and tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo&lt;/strong&gt;: Simplified the development process and provided access to native APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Navigation&lt;/strong&gt;: For smooth navigation between screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reanimated&lt;/strong&gt;: For fluid animations and transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redux&lt;/strong&gt;: To manage the global state, including user progress, leaderboards, and campaign data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backend: NestJS (Node.js Framework)
&lt;/h3&gt;

&lt;p&gt;The backend is powered by &lt;strong&gt;NestJS&lt;/strong&gt;, a progressive Node.js framework built with &lt;strong&gt;TypeScript&lt;/strong&gt;. NestJS provided a modular structure, making the backend easier to maintain and scale.&lt;/p&gt;

&lt;p&gt;Backend highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RESTful APIs&lt;/strong&gt;: Exposed endpoints for user authentication, leaderboard management, and campaign data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt;: The relational database for storing user data, scores, and game content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt;: For caching frequently accessed data like leaderboards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT Authentication&lt;/strong&gt;: Secured user sessions with JSON Web Tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cloud &amp;amp; Hosting
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS (Amazon Web Services)&lt;/strong&gt;: Hosted the backend and database for scalability and reliability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt;: Handled CDN and DDoS protection to ensure fast and secure gameplay.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immersive Gameplay&lt;/strong&gt;: Watch high-quality street walk videos and guess the location.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Campaigns&lt;/strong&gt;: Explore themed challenges like "Southern Europe's Sunlit Secrets" or "Unlock the UK's Royal Secrets."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer Modes&lt;/strong&gt;: Compete with friends in duels or join global tournaments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboards&lt;/strong&gt;: Track your progress and aim to become the "King/Queen of the World."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collectibles&lt;/strong&gt;: Earn badges and unlock monuments as you play.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Platform Compatibility&lt;/strong&gt;: React Native made cross-platform development easier, but handling platform-specific quirks required extra attention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video Optimization&lt;/strong&gt;: Ensuring smooth playback of street walk videos on both high-end and low-end devices was challenging. I used adaptive bitrate streaming to address this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: With an ever-growing user base, designing a backend that scales while maintaining fast response times was crucial. Redis caching was a game-changer here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localization&lt;/strong&gt;: Adding support for languages like Spanish (ES) and Portuguese (PT) helped make the app accessible to a broader audience.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why "Where Am I?" Stands Out
&lt;/h2&gt;

&lt;p&gt;While GeoGuessr and CityGuessr are fantastic games, &lt;em&gt;Where Am I?&lt;/em&gt; offers a unique twist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Completely Free&lt;/strong&gt;: No paywalls or subscriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-First&lt;/strong&gt;: Designed specifically for Android and iOS devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immersive Campaigns&lt;/strong&gt;: Themed challenges that take players on curated adventures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community-Centric&lt;/strong&gt;: Player feedback drives new updates and features.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The journey doesn’t stop here! I’m actively working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom Multiplayer Modes&lt;/strong&gt;: Letting players create their own rules and challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Campaigns&lt;/strong&gt;: Expanding the game with more countries and themes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Augmented Reality (AR)&lt;/strong&gt;: Exploring ways to integrate AR for an even more immersive experience.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;If you’re looking for a fun, educational, and &lt;a href="https://whereamigame.app/" rel="noopener noreferrer"&gt;free GeoGuessr alternative&lt;/a&gt;, give &lt;em&gt;Where Am I?&lt;/em&gt; a try. I’d love to hear your feedback and ideas for future updates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://whereamigame.app" rel="noopener noreferrer"&gt;whereamigame.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.adex77.WhereAmI" rel="noopener noreferrer"&gt;Download on Android&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;App Store: &lt;a href="https://apps.apple.com/us/app/where-am-i-explore-the-world/id6657987209" rel="noopener noreferrer"&gt;Download on iOS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for reading! If you have any questions about the tech stack or the development process, feel free to drop them in the comments below. 🙌&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>nestjs</category>
      <category>mobile</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
