{"id":13941277,"url":"https://github.com/ghdna/athena-express","last_synced_at":"2025-05-15T19:05:46.626Z","repository":{"id":32870399,"uuid":"144792887","full_name":"ghdna/athena-express","owner":"ghdna","description":"Athena-Express can simplify executing SQL queries in Amazon Athena AND fetching cleaned-up JSON results in the same synchronous or asynchronous request - well suited for web applications.","archived":false,"fork":false,"pushed_at":"2024-05-14T09:07:13.000Z","size":219,"stargazers_count":182,"open_issues_count":29,"forks_count":71,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-30T23:41:35.957Z","etag":null,"topics":["amazon-athena","amazon-web-services","athena-express","aws","aws-athena","aws-lambda","aws-sdk","json","json-api","node-js","sql"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/athena-express","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ghdna.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-08-15T01:58:29.000Z","updated_at":"2025-04-04T21:52:12.000Z","dependencies_parsed_at":"2023-01-14T22:29:30.191Z","dependency_job_id":"8c6d9e9c-223a-4152-aaa3-1d9838044928","html_url":"https://github.com/ghdna/athena-express","commit_stats":{"total_commits":127,"total_committers":14,"mean_commits":9.071428571428571,"dds":0.2834645669291339,"last_synced_commit":"35fb1d28e1dc64e60be4ea8f0d1388223c3744de"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghdna%2Fathena-express","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghdna%2Fathena-express/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghdna%2Fathena-express/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghdna%2Fathena-express/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ghdna","download_url":"https://codeload.github.com/ghdna/athena-express/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253638975,"owners_count":21940434,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["amazon-athena","amazon-web-services","athena-express","aws","aws-athena","aws-lambda","aws-sdk","json","json-api","node-js","sql"],"created_at":"2024-08-08T02:01:15.462Z","updated_at":"2025-05-15T19:05:46.603Z","avatar_url":"https://github.com/ghdna.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# Athena-Express: Simplifying SQL queries on Amazon Athena\n\n[![NPM](https://nodei.co/npm/athena-express.png?compact=true)](https://nodei.co/npm/athena-express/)\n\n[![Build Status](https://travis-ci.org/ghdna/athena-express.svg?branch=master)](https://travis-ci.org/ghdna/athena-express)\n[![Package Quality](http://npm.packagequality.com/shield/athena-express.png)](http://packagequality.com/#?package=athena-express)\n[![Code Climate](https://codeclimate.com/github/ghdna/athena-express/badges/gpa.svg)](https://codeclimate.com/github/ghdna/athena-express/)\n[![Coverage Status](https://coveralls.io/repos/github/ghdna/athena-express/badge.svg?branch=master)](https://coveralls.io/github/ghdna/athena-express?branch=master)\n[![Downloads](https://img.shields.io/npm/dt/athena-express.svg)](https://www.npmjs.com/package/athena-express)\n\n###### _As published on the official [AWS Partner Network Blog](https://aws.amazon.com/blogs/apn/using-athena-express-to-simplify-sql-queries-on-amazon-athena/)_\n\n## Synopsis\n\nAthena-Express can simplify executing SQL queries in Amazon Athena **AND** fetching _cleaned-up_ JSON results in the same synchronous or asynchronous request - well suited for web applications. \n\n\n##### Example:\n\u003cimg src=\"https://image.ibb.co/cWNvFV/carbon-1.png\" alt=\"Athena-Express Example\" width=\"700\"\u003e\n\n\n\n\n\n## Amazon Athena Background\n\n[Amazon Athena](https://aws.amazon.com/athena/), launched at AWS re:Invent 2016, made it easier to analyze data in Amazon S3 using standard SQL. Under the covers, it uses [Presto](https://prestodb.io/), which is an opensource SQL engine developed by Facebook in 2012 to query their 300 Petabyte data warehouse. It's incredibly powerful!\n\nAmazon Athena combines the strength of Presto with serverless \u0026 self-managed capabilities of AWS. By simply pointing Athena to your data in Amazon S3, one could start querying using standard SQL. Most results are delivered within seconds and there’s no need for complex ETL jobs to prepare your data for analysis. This makes it easy for anyone with SQL skills to quickly analyze large-scale datasets.\n\n## How athena-express simplifies using Amazon Athena\n\n`athena-express` simplifies integrating Amazon Athena with any Node.JS application - running as a standalone application or as a Lambda function. As a wrapper on AWS SDK, Athena-Express  bundles the following steps listed on the official [AWS Documentation](https://docs.aws.amazon.com/athena/latest/APIReference/Welcome.html):\n\n1.\tInitiates a query execution\n2.\tKeeps checking until the query has finished executing\n3.\tFetches the results of the query execution from Amazon S3\n\nAnd as added features\n\n4.\tFormats the results into a clean, user-friendly JSON array\n5.\tHandles specific Athena errors by recursively retrying for `ThrottlingException`, `NetworkingError`, and `TooManyRequestsException`\n6.\tProvides optional helpful stats including cost per query in USD\n7.\tFetching results (rows) via Pagination OR as a continuous stream\n8.\tSynchrnous and Asynchornous fetching of results (rows)\n\nIntegrating with Amazon Athena without `athena-express` would require you to identify the appropriate API methods in the AWS SDK, stich them together sequentially, and then build out an error handling \u0026 retry mechanism for each of those methods. \n\n\n\u003e`athena-express` can help you save time \u0026 effort in setting up this integration so that you can focus on core application development. \n\n\n### How is athena-express being used?\nThe most common use case is integrating a web front-end with Amazon Athena using `athena-express` as a backend. This backend could be any Node.JS application that could be hosted locally, or on an EC2 instance, or AWS Lambda.\n\nHere is an example using AWS Lambda: \n\u003cimg src=\"https://image.ibb.co/k3RpNA/Screen-Shot-2018-11-22-at-11-17-58-AM.pngg\" alt=\"athena-express architecture\" width=\"700\"\u003e\n\nThis architecture has a web front-end that invokes an API endpoint hosted on Amazon API Gateway by passing a query request. The query request can be as simple as `SELECT * FROM movies LIMIT 3`\n\nThis API Gateway then triggers a Lambda function that has the `athena-express` library imported. \n\n\n## Setup\n\n### Prerequisites\n\n-   You will need either an `IAM Role` (if you're running `athena-express` on AWS Lambda or AWS EC2) **OR** an `IAM User` with `accessKeyId` and `secretAccessKey` (if you're running `athena-express` on a standalone NodeJS application)\n-   This IAM role/user must have `AmazonAthenaFullAccess` and `AmazonS3FullAccess` policies attached \n    -   Note: As an alternative to granting `AmazonS3FullAccess` you could granularize and limit write access to a specific `bucket`. Just specify this bucket name during `athena-express` initialization\n\n### Configuration\n- `athena-express` needs an AWS SDK object created with relevant permissions as mentioned in the prerequisites above.\n- This AWS object is passed within the constructor so that it can invoke Amazon Athena SDK. It's up to you how you create this `aws` object. Here are 4 options: \n\t1. Create an `aws` object by explicitly passing in the `accessKeyId` and `secretAccessKey` generated in prerequisites\n\t```javascript \n    const AWS = require(\"aws-sdk\");\n    const awsCredentials = {\n        region: \"YOUR_AWS_REGION\",\n\t\taccessKeyId: \"YOUR_AWS_ACCESS_KEY_ID\",\n\t\tsecretAccessKey: \"YOUR_AWS_SECRET_ACCESS_KEY\"\n    };\n    AWS.config.update(awsCredentials);\n\n    const athenaExpressConfig = { aws: AWS }; //configuring athena-express with aws sdk object\n    const athenaExpress = new AthenaExpress(athenaExpressConfig);\n\t```\n\t2. OR if using Lambda, provide an [IAM execution role](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/using-lambda-iam-role-setup.html) with `AmazonAthenaFullAccess` and `AmazonS3FullAccess` policies attached\n \t```javascript \n    const AWS = require(\"aws-sdk\");\n\tconst athenaExpressConfig = { aws: AWS }; //configuring athena-express with aws sdk object\n    const athenaExpress = new AthenaExpress(athenaExpressConfig);\n\t```  \n\t3. OR Use [instance profiles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) when using EC2s\n\t4. OR Use [environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html)\n\n\n\n#### Simple configuration\n\n- Simple configuration requires only the AWS SDK object to be passed as a parameter to initialize `athena-express`\n- Default values are assumed for all parameter options and `athena-express` creates a new `S3 bucket` in your AWS account for Amazon Athena to store the query results in.\n\n```javascript\nconst AWS = require(\"aws-sdk\");\n\nconst athenaExpressConfig = { aws: AWS }; //simple configuration with just an aws sdk object\n\n//Initializing athena-express\nconst athenaExpress = new AthenaExpress(athenaExpressConfig);\n```\n\n\n#### Advance configuration\n\n- Besides the `aws` sdk paramater that is required, you can add any of the following optional parameters below\n\n\n\n```javascript\nconst AWS = require(\"aws-sdk\");\n\n//Example showing all Config parameters.\nconst athenaExpressConfig = {\n\taws: AWS, // required \n\ts3: \"s3://mybucketname\", // optional \n\tdb: \"myDbName\", // optional\n\tworkgroup: \"myWorkGroupName\", // optional\n\tformatJson: true, // optional\n\tretry: 200, // optional\n\tgetStats: true, // optional\n\tignoreEmpty: true, // optional\n\tencryption: { EncryptionOption: \"SSE_KMS\", KmsKey: process.env.kmskey}, // optional\n\tskipResults: false, // optional\n\twaitForResults: false, // optional\n\tcatalog: \"hive\" //optional\n};\n\n//Initializing AthenaExpress\nconst athenaExpress = new AthenaExpress(athenaExpressConfig);\n```\n\n###### Advance config Parameters:\n\n| Parameter  | Format | Default Value | Description |\n| ------------- | ------------- | ------------- | ------------- |\n| s3 | string  | `athena-express` creates a new bucket for you  | The location in Amazon S3 where your query results are stored, such as `s3://path/to/query/bucket/`. \u003cbr /\u003e `athena-express` will create a new bucket for you if you don't provide a value for this param but sometimes that could cause an issue if you had recently deleted a bucket with the same name. (something to do with cache). When that happens, just specify you own bucket name. Alternatively you can also use `workgroup`.   |\n| db | string  | `default`  | Athena database name that the SQL queries should be executed in. When a `db` name is specified in the config, you can execute SQL queries without needing to explicitly mention DB name. e.g. \u003cbr /\u003e` athenaExpress.query(\"SELECT * FROM movies LIMIT 3\")` \u003cbr /\u003e as opposed to \u003cbr /\u003e` athenaExpress.query({sql: \"SELECT * FROM movies LIMIT 3\", db: \"moviedb\"});`  |\n| workgroup | string  | `primary`  | The name of the workgroup in which the query is being started. \u003cbr /\u003e Note: athena-express cannot create workgroups (as it includes a lot of configuration) so you will need to create one beforehand IFF you intend to use a non default workgroup. Learn More here. [Setting up Workgroups](https://docs.aws.amazon.com/athena/latest/ug/user-created-workgroups.html) |\n|formatJson  | boolean | `true` |  Override as false if you rather get the raw unformatted output from S3. |\n|retry  | integer | `200` milliseconds| Wait interval between re-checking if the specific Athena query has finished executing |\n|getStats | boolean | `false`| Set `getStats: true` to capture additional metadata for your query, such as: \u003cul\u003e\u003cli\u003e`EngineExecutionTimeInMillis`\u003c/li\u003e\u003cli\u003e`DataScannedInBytes`\u003c/li\u003e\u003cli\u003e`TotalExecutionTimeInMillis`\u003c/li\u003e\u003cli\u003e`QueryQueueTimeInMillis`\u003c/li\u003e\u003cli\u003e`QueryPlanningTimeInMillis`\u003c/li\u003e\u003cli\u003e`ServiceProcessingTimeInMillis`\u003c/li\u003e\u003cli\u003e`DataScannedInMB`\u003c/li\u003e\u003cli\u003e`QueryCostInUSD`\u003c/li\u003e\u003cli\u003e`Count`\u003c/li\u003e\u003cli\u003e`QueryExecutionId`\u003c/li\u003e\u003cli\u003e`S3Location`\u003c/li\u003e\u003c/ul\u003e |\n|ignoreEmpty  | boolean | `true`| Ignore fields with empty values from the final JSON response.  |\n|encryption | object | -- | [Encryption configuation](https://docs.aws.amazon.com/athena/latest/ug/encryption.html) example usage: \u003cbr /\u003e`{ EncryptionOption: \"SSE_KMS\", KmsKey: process.env.kmskey}` |\n|skipResults | boolean | `false` | For a unique requirement where a user may only want to execute the query in Athena and store the results in S3 but NOT fetch those results in that moment. \u003cbr /\u003ePerhaps to be retrieved later or simply stored in S3 for auditing/logging purposes. \u003cbr /\u003eTo retrieve the results, you can simply pass the `QueryExecutionId` into athena-express as such: `athenaExpress.query(\"ab493e66-138f-4b78-a187-51f43fd5f0eb\")`  |\n|waitForResults  | boolean | `true` | When low latency is the objective, you can skip waiting for a query to be completed in Athena. Returns `QueryExecutionId`, which you can pass into athena-express later as such: `athenaExpress.query(\"ab493e66-138f-4b78-a187-51f43fd5f0eb\")` \u003cbr /\u003e Not to be confused with `skipResults`, which actually waits for the query to be completed before returning `QueryExecutionId` and other stats. `waitForResults` is meant for fire-and-forget kind of operations.  \u003cbr /\u003e  |\n|catalog  | string | `null` | The catalog to which the query results belong  |\n\n\n\n\n###### Advance Query Parameters:\n```javascript\n//Example showing all Query parameters.\nlet myQuery = {\n    sql: \"SELECT * FROM elb_logs LIMIT 3\" // required,\n    db: \"sampledb\", // optional. \n    pagination: 5, //optional\n    NextToken: \"ARfCDXRjMk...\", //optional\n    QueryExecutionId: \"c274843b-4c5c-4ccf-ac8b-e33d595b927d\", //optional\n    catalog: \"hive\" //optional\n};\n```\n\n| Parameter  | Format | Default Value | Description |\n| ------------- | ------------- | ------------- | ------------- |\n| sql  | string \u003cbr /\u003e `required`  |  | The SQL query statements to be executed. \u003cbr /\u003e E.g. \"SELECT * FROM elb_logs LIMIT 3  |\n| db | string \u003cbr /\u003e   | `default` |The name of the database used in the query execution. \u003cbr /\u003e You can specify the database name here within the query itself OR in athenaExpressConfig during initialization as shown in [Advance Config Parameters](https://github.com/ghdna/athena-express#advance-config-parameters) |\n| pagination | number  | `0` \u003cbr /\u003e max: `1000` | Maximum number of results (rows) to return in a single paginated response. \u003cbr /\u003eResponse includes results from page 1 along with `NextToken` and `QueryExecutionId` IFF the response was truncated \u003cbr /\u003e To obtain the next set of pages, pass in the `NextToken` and `QueryExecutionId` back to Athena. \u003cbr /\u003e See [example here](https://github.com/ghdna/athena-express#more-examples)\t |\n| NextToken | string  | `null` | A token generated by the Athena service that specifies where to continue pagination if a previous request was truncated. To obtain the next set of pages, pass in the NextToken from the response object of the previous page call.  |\n| QueryExecutionId | string  | `null` | The unique ID of the query execution. \u003cbr /\u003eTo be passed into the AthenaExpress query when using the features of `Pagination`, `waitForResults` or `skipResults `  |\n|catalog  | string | `null` | The catalog to which the query results belong  |\n\n## Usage: Invoking athena-express\n\n###### Using Promises to query Athena:\n\n```javascript\n/*Option 1: object notation*/ \nlet myQuery = {\n\tsql: \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\" /* required */,\n\tdb: \"sampledb\" /* optional. You could specify a database here or in the advance configuration option mentioned above*/\n};\n\n/*OR Option 2: string notation*/ \nlet myQuery = \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\"\n\nathenaExpress\n\t.query(myQuery)\n\t.then(results =\u003e {\n\t\tconsole.log(results);\n\t})\n\t.catch(error =\u003e {\n\t\tconsole.log(error);\n\t});\n```\n\n###### Using Async/Await to query Athena:\n\n```javascript\n(async () =\u003e {\n/*Option 1: object notation*/ \n\tlet myQuery = {\n\t\tsql: \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\" /* required */,\n\t\tdb: \"sampledb\" /* optional. You could specify a database here or in the configuration constructor*/\n\t};\n    \n/*OR Option 2: string notation*/ \nlet myQuery = \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\"\n\n\ttry {\n\t\tlet results = await athenaExpress.query(myQuery);\n\t\tconsole.log(results);\n\t} catch (error) {\n\t\tconsole.log(error);\n\t}\n})();\n```\n###### Using QueryExecutionID:\n\nApplicable only if you already have the `QueryExecutionID` from an earlier execution. See `skipResults` or `waitForResults` in the advance config params above to learn more.\n```javascript\nconst myQueryExecutionId = \"bf6ffb5f-6c36-4a66-8735-3be6275960ae\";\nlet results = await athenaExpress.query(myQueryExecutionId);\nconsole.log(results);\n```\n## Full Examples\n\n###### Using a standalone NodeJS application\n\n```javascript\n\"use strict\";\n\nconst AthenaExpress = require(\"athena-express\"),\n\tAWS = require(\"aws-sdk\"),\n\tawsCredentials = {\n\t\tregion: \"YOUR_AWS_REGION\",\n\t\taccessKeyId: \"YOUR_AWS_ACCESS_KEY_ID\",\n\t\tsecretAccessKey: \"YOUR_AWS_SECRET_ACCESS_KEY\"\n\t};\n\nAWS.config.update(awsCredentials);\n\nconst athenaExpressConfig = {\n\taws: AWS,\n\ts3: \"s3://my-bucket-for-storing-athena-results-us-east-1\",\n\tgetStats: true\n};\n\nconst athenaExpress = new AthenaExpress(athenaExpressConfig);\n\n//Invoking a query on Amazon Athena\n(async () =\u003e {\n\tlet myQuery = {\n\t\tsql: \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\",\n\t\tdb: \"sampledb\"\n\t};\n\n\ttry {\n\t\tlet results = await athenaExpress.query(myQuery);\n\t\tconsole.log(results);\n\t} catch (error) {\n\t\tconsole.log(error);\n\t}\n})();\n```\n\n###### Using AWS Lambda\n\n```javascript\n\"use strict\";\n\nconst AthenaExpress = require(\"athena-express\"),\n\tAWS = require(\"aws-sdk\");\n\n\t/* AWS Credentials are not required here \n    /* Make sure the IAM Execution Role used by this Lambda \n    /* has the necessary permission to execute Athena queries \n    /* and store the result in Amazon S3 bucket\n    /* See configuration section above under Setup for more info */\n\nconst athenaExpressConfig = {\n\taws: AWS,\n\tdb: \"sampledb\",\n\tgetStats: true\n};\nconst athenaExpress = new AthenaExpress(athenaExpressConfig);\n\nexports.handler = async event =\u003e {\n\tconst sqlQuery = \"SELECT elb_name, request_port, request_ip FROM elb_logs LIMIT 3\";\n\n\ttry {\n\t\tlet results = await athenaExpress.query(sqlQuery);\n\t\treturn results;\n\t} catch (error) {\n\t\treturn error;\n\t}\n};\n```\n\n###### Results:\n\n\u003cimg src=\"https://image.ibb.co/fpARNA/carbon-2.png\" alt=\"Athena-Express result\" width=\"400\"\u003e\n\n## More Examples\n##### Pagination\n######  Query to fetch results (rows) for page 1\n```javascript\nasync function main() {\n     const myQuery = {\n        sql: \"SELECT * from students LIMIT 100\",\n        pagination: 10\n    };\n    let results = await athenaExpress.query(myQuery);\n    console.log(results);\n}\nmain();\n\n```\nThis will fetch the first 10 results (rows) off the 100 that exits in Athena. To query the next 10 rows, pass the values for `NextToken` and `QueryExecutionId` that were returned in the first query.\n\n###### Query to fetch results (rows) for page 2 and beyond\n```javascript\nasync function main() {\n     const myQuery = {\n        sql: \"SELECT * from students LIMIT 100\",\n        pagination: 10,\n        NextToken: \"ARfCDXRjMkQsR1NWziK1ARgiip3umf3q0/bZmNZWeQxUDc7iSToT7uJHy2yo8nL5FyxQoIIkuPh/zDD51xld7SoALA+zhMhpZg==\",\n        QueryExecutionId: \"c274843b-4c5c-4ccf-ac8b-e33d595b927d\",\n    };\n    let results = await athenaExpress.query(myQuery);\n    console.log(results);\n}\nmain();\n\n```\n\n\n##### UTILITY queries\n###### Show Tables (single column result)\n ```javascript\n const results = await athenaExpress.query(\"SHOW TABLES\");\nconsole.log(results);\n\n//Output:\n{ Items:\n   [ { row: 'default' },\n     { row: 'sampledb' } ] }\n   ```\n\n###### Describe Table (dual column result)\n\n ```javascript\n const results = await athenaExpress.query(\"DESCRIBE elb_logs\");\nconsole.log(results);\n\n//Output:\n{ Items:\n   [ { request_timestamp: 'string' },\n     { elb_name: 'string' },\n     { request_ip: 'string' },\n     { request_port: 'int' },\n     { backend_ip: 'string' },\n     { backend_port: 'int' },\n     { request_processing_time: 'double' },\n     { backend_processing_time: 'double' },\n     { client_response_time: 'double' },\n     { elb_response_code: 'string' },\n     { backend_response_code: 'string' },\n     { received_bytes: 'bigint' },\n     { sent_bytes: 'bigint' },\n     { request_verb: 'string' },\n     { url: 'string' },\n     { protocol: 'string' },\n     { user_agent: 'string' },\n     { ssl_cipher: 'string' },\n     { ssl_protocol: 'string' } ] }\n   ```\n\n## Contributors\n\n[Gary Arora](https://twitter.com/AroraGary)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fghdna%2Fathena-express","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fghdna%2Fathena-express","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fghdna%2Fathena-express/lists"}