{"id":16939204,"url":"https://github.com/07akioni/lyla","last_synced_at":"2025-04-09T13:04:33.599Z","repository":{"id":39092041,"uuid":"473702445","full_name":"07akioni/lyla","owner":"07akioni","description":"A fully typed HTTP client with explicit behavior \u0026 error handling.","archived":false,"fork":false,"pushed_at":"2025-02-27T12:38:32.000Z","size":551,"stargazers_count":68,"open_issues_count":0,"forks_count":6,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-02T07:52:27.588Z","etag":null,"topics":["http-client","http-request","request","xhr","xmlhttprequest"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/07akioni.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2022-03-24T17:13:23.000Z","updated_at":"2025-03-19T05:13:25.000Z","dependencies_parsed_at":"2023-09-23T14:37:20.432Z","dependency_job_id":"69722b81-8532-4360-857f-ef3f0c115717","html_url":"https://github.com/07akioni/lyla","commit_stats":{"total_commits":326,"total_committers":7,"mean_commits":46.57142857142857,"dds":"0.030674846625766916","last_synced_commit":"eafd064e32da6b0f588d18dc413046b2ff6b794d"},"previous_names":[],"tags_count":57,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/07akioni%2Flyla","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/07akioni%2Flyla/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/07akioni%2Flyla/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/07akioni%2Flyla/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/07akioni","download_url":"https://codeload.github.com/07akioni/lyla/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247749963,"owners_count":20989714,"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":["http-client","http-request","request","xhr","xmlhttprequest"],"created_at":"2024-10-13T21:03:59.597Z","updated_at":"2025-04-09T13:04:33.569Z","avatar_url":"https://github.com/07akioni.png","language":"TypeScript","readme":"# lyla · [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![ci](https://github.com/07akioni/lyla/actions/workflows/node.js.yml/badge.svg)](https://github.com/07akioni/lyla/actions/workflows/node.js.yml/badge.svg) [![npm version](https://badge.fury.io/js/lyla.svg)](https://badge.fury.io/js/lyla) [![minzipped size](https://badgen.net/bundlephobia/minzip/lyla)](https://badgen.net/bundlephobia/minzip/lyla)\n\nA fully typed HTTP client with explicit behavior \u0026 error handling.\n\n\u003e [!IMPORTANT]  \n\u003e If you only want to support browser environment, you should use package `@lylajs/web` instead of `lyla`.\n\n| Environment          | Package           | Note                                                                                            |\n| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |\n| web                  | `@lylajs/web`     |                                                                                                 |\n| node                 | `@lylajs/node`    |                                                                                                 |\n| toutiao miniprogram  | `@lylajs/tt`      |                                                                                                 |\n| weixin miniprogram   | `@lylajs/wx`      |                                                                                                 |\n| qq miniprogram       | `@lylajs/qq`      |                                                                                                 |\n| zhifubao miniprogram | `@lylajs/my`      |                                                                                                 |\n| uni-app              | `@lylajs/uni-app` |                                                                                                 |\n| web + nodejs         | `lyla`            | Unless you have explicit cross-platform isomorphic requirements, please don't use this package. |\n\nEnglish · [中文](https://github.com/07akioni/lyla/blob/main/README.zh_CN.md)\n\n- Won't share options between different instances, which means your reqeust won't be unexpectedly modified.\n- Won't transform response body implicitly (For example transform invalid JSON to string).\n- Won't suppress expection silently (JSON parse error, config error, eg.).\n- [Explicit error handling](#error-handling).\n- Supports typescript for response data.\n- Supports upload progress (which isn't supported by fetch API).\n- Friendly error tracing (with sync trace, you can see where the request is sent on error).\n- Access typed custom context object in the whole process.\n\nFor difference compared with other libs, see [FAQ](#faq).\n\n## Installation\n\n```bash\n# you can install `lyla` or `@lylajs/xxx`\nnpm i @lylajs/web # for npm\npnpm i @lylajs/web # for pnpm\nyarn add @lylajs/web # for yarn\n```\n\n## Usage\n\n\u003e [!IMPORTANT]  \n\u003e Lyla use **`json`** field to config request data, not `body`! Also, there's **no** `data` field in lyla.\n\u003e\n\u003e `body` field is used to set raw body of the request such as `string` or `Blob`, which is not a common case.\n\n```ts\nimport { createLyla } from '@lylajs/web'\n\n// For request which need default options, hooks or custom context info,\n// it's recommended using `createLyla` to create lyla instance.\n// If you only need the simplest usage, you can use\n// import { lyla } from '@lylajs/web'\nconst { lyla } = createLyla({ context: null })\nconst { json } = await lyla.post('https://example.com', {\n  json: { foo: 'bar' }\n})\n\n// TypeScript\ntype MyType = {}\n\n// `json`'s type is `MyType`\nconst { json } = await lyla.post\u003cMyType\u003e('https://example.com', {\n  json: { foo: 'bar' }\n})\n```\n\n## API\n\n### `createLyla(options, ...overrides)`\n\n```ts\nfunction createLyla\u003cC\u003e(\n  options: LylaRequestOptions\u003cC\u003e \u0026 { context: C },\n  ...overrides: LylaRequestOptions\u003cC\u003e[]\n): { lyla: Lyla; isLylaError: (e: unknown) =\u003e e is LylaError }\n```\n\n### `lyla\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.get\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.post\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.put\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.patch\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.head\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.delete\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.connect\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.options\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.trace\u003cT\u003e(options: LylaRequestOptions): LylaResponse\u003cT\u003e`\n\n### `lyla.withRetry(options: LylaWithRetryOptions) =\u003e Lyla`\n\nBeta compatibility. Currently only work in `2.0.0-beta.1` and later versions.\n\nCreate a `lyla` instance with retry feature using existing `lyla` instance. For detail see [Retry request](#Retry request).\n\n#### Type `LylaRequestOptions`\n\n```ts\ntype LylaRequestOptions\u003cC = undefined\u003e = {\n  url?: string\n  method?:\n    | 'get'\n    | 'GET'\n    | 'post'\n    | 'POST'\n    | 'put'\n    | 'PUT'\n    | 'patch'\n    | 'PATCH'\n    | 'head'\n    | 'HEAD'\n    | 'delete'\n    | 'DELETE'\n    | 'options'\n    | 'OPTIONS'\n  timeout?: number\n  /**\n   * True when credentials are to be included in a cross-origin request.\n   * False when they are to be excluded in a cross-origin request and when\n   * cookies are to be ignored in its response.\n   */\n  withCredentials?: boolean\n  headers?: LylaRequestHeaders\n  /**\n   * Type of `response.body`.\n   */\n  responseType?: 'arraybuffer' | 'blob' | 'text'\n  body?: XMLHttpRequestBodyInit\n  /**\n   * JSON value to be written into the request body. It can't be used with\n   * `body`.\n   */\n  json?: any\n  /**\n   * Query object, also known as search params.\n   * Note, if you want to set `null` or `undefined` as value in query,\n   * use object like `query: { key: \"undefined\" }` instead of `query: { key: undefined }`.\n   * Otherwise, the k-v pair will be ignored.\n   */\n  query?: Record\u003c\n    string,\n    | string\n    | number\n    | boolean\n    | Array\u003cstring | number | boolean\u003e\n    | null\n    | undefined\n  \u003e\n  baseUrl?: string\n  /**\n   * Abort signal of the request.\n   */\n  signal?: AbortSignal\n  onUploadProgress?: (progress: LylaProgress\u003cC\u003e) =\u003e void\n  onDownloadProgress?: (progress: LylaProgress\u003cC\u003e) =\u003e void\n  /**\n   * Whether to allow get request with body. Default is false.\n   * It's not recommended to use GET request with body since it doesn't conform HTTP\n   * specification.\n   */\n  allowGetBody?: boolean\n  hooks?: {\n    /**\n     * Callbacks fired when options is passed into the request. In this moment,\n     * request options haven't be normalized.\n     */\n    onInit?: Array\u003c\n      (\n        options: LylaRequestOptions\u003cC\u003e\n      ) =\u003e LylaRequestOptions\u003cC\u003e | Promise\u003cLylaRequestOptions\u003cC\u003e\u003e\n    \u003e\n    /**\n     * Callbacks fired before request is sent. In this moment, request options is\n     * normalized.\n     */\n    onBeforeRequest?: Array\u003c\n      (\n        options: LylaRequestOptions\u003cC\u003e\n      ) =\u003e LylaRequestOptions\u003cC\u003e | Promise\u003cLylaRequestOptions\u003cC\u003e\u003e\n    \u003e\n    /**\n     * Callbacks fired after headers are received.\n     *\n     * only work in @lylajs/web @lylajs/node and lyla.\n     */\n    onHeadersReceived?: Array\u003c\n      (\n        payload: {\n          headers: Record\u003cstring, string\u003e\n          originalRequest: M['originalRequest']\n          requestOptions: LylaRequestOptionsWithContext\u003cC\u003e\n        },\n        reject: (reason: unknown) =\u003e void\n      ) =\u003e void\n    \u003e\n    /**\n     * Callbacks fired after response is received.\n     */\n    onAfterResponse?: Array\u003c\n      (\n        response: LylaResponse\u003cany\u003e,\n        reject: (reason: unknown) =\u003e void\n      ) =\u003e LylaResponse\u003cany\u003e | Promise\u003cLylaResponse\u003cany\u003e\u003e\n    \u003e\n    /**\n     * Callbacks fired when there's error while response handling. It's only\n     * fired by LylaError. Error thrown by user won't triggered the callback,\n     * for example if user throws an error in `onAfterResponse` hook. The\n     * callback won't be fired.\n     *\n     * Before the callback if finished, the error won't be thrown.\n     */\n    onResponseError?: Array\u003c\n      (\n        error: LylaResponseError\u003cC\u003e,\n        reject: (reason: unknown) =\u003e void\n      ) =\u003e void | Promise\u003cvoid\u003e\n    \u003e\n    /**\n     * Callbacks fired when a non-response error occurs (except\n     * BROKEN_ON_NON_RESPONSE_ERROR)\n     */\n    onNonResponseError?: Array\u003c\n      (error: LylaNonResponseError\u003cC\u003e) =\u003e void | Promise\u003cvoid\u003e\n    \u003e\n  }\n  /**\n   * Custom context of the request.\n   */\n  context?: C\n  /**\n   * Extra requestion options, these options will be passed to the corresponding\n   * request method of the platform. Its type depends on the platform.\n   */\n  extraOptions?: {}\n}\n```\n\n#### Type `LylaResponse`\n\n```ts\ntype LylaResponse\u003cT = any, C = undefined\u003e = {\n  requestOptions: LylaRequestOptions\u003cC\u003e\n  status: number\n  statusText: string\n  /**\n   * Headers of the response. All the keys are in lower case.\n   */\n  headers: Record\u003cstring, string\u003e\n  /**\n   * Response body.\n   */\n  body: PlatformRelevant\n  /**\n   * JSON value of the response. If body is not valid JSON text, access the\n   * field will cause an error.\n   */\n  json: T\n  /**\n   * Custom context of the request\n   */\n  context?: C\n}\n```\n\n#### Type `LylaProgress`\n\n```ts\ntype LylaProgress\u003cC\u003e = {\n  /**\n   * Percentage of the progress. From 0 to 100.\n   */\n  percent: number\n  /**\n   * Loaded bytes of the progress.\n   */\n  loaded: number\n  /**\n   * Total bytes of the progress. If progress is not length-computable it would\n   * be 0.\n   */\n  total: number\n  /**\n   * Whether the total bytes of the progress is computable.\n   */\n  lengthComputable: boolean\n  /**\n   * Request options of the request.\n   */\n  requestOptions: LylaRequestOptions\u003cC\u003e\n}\n```\n\n#### Type `LylaRequestHeaders`\n\n```ts\ntype LylaRequestHeaders = Record\u003cstring, string | number | undefined\u003e\n```\n\nRequest headers can be `string`, `number` or `undefined`. If it's `undefined`,\nit would override default options' headers. For example:\n\n```ts\nimport { createLyla } from '@lylajs/web'\n\nconst { lyla } = createLyla({ headers: { foo: 'bar' }, context: null })\n\n// Request won't have the `foo` header\nrequest.get('http://example.com', { headers: { foo: undefined } })\n```\n\n## Error handling\n\n```ts\nimport { createLyla, LYLA_ERROR } from '@lylajs/web'\n\nconst { lyla, isLylaError } = createLyla({ context: null })\n\ntry {\n  const { json } = await lyla.get('https://example.com')\n  // ...\n} catch (e) {\n  if (isLylaError(e)) {\n    e.type\n    // ...\n  } else {\n    // ...\n  }\n}\n```\n\n### Type `LylaError`\n\n```ts\n// This is not a percise definition, it platform relavant. For full definition,\n// see https://github.com/07akioni/lyla/blob/main/packages/core/src/error.ts\ntype LylaError\u003cC = undefined\u003e = {\n  name: string\n  message: string\n  type: LYLA_ERROR\n  // LylaError's corresponding original error. Normally it's useless, only\n  // invalid JSON will set this field. In most time, you may need `detail` field.\n  error: Error | undefined\n  detail: PlatformRelevant // Fail info generated by specific platform\n  response: PlatformRelevant // Like LylaResponse | undefined\n  // Custom context of the request\n  context: C\n}\n```\n\n### `LYLA_ERROR`\n\n```ts\nexport enum LYLA_ERROR {\n  /**\n   * Request encountered an error, fired by XHR `onerror` event. It doesn't mean\n   * your network has error, for example CORS error also triggers NETWORK_ERROR.\n   */\n  NETWORK = 'NETWORK',\n  /**\n   * Request is aborted.\n   */\n  ABORTED = 'ABORTED',\n  /**\n   * Response text is not valid JSON.\n   */\n  INVALID_JSON = 'INVALID_JSON',\n  /**\n   * Trying resolving `response.json` with `responseType='arraybuffer'` or\n   * `responseType='blob'`.\n   */\n  INVALID_CONVERSION = 'INVALID_CONVERSION',\n  /**\n   * Request timeout.\n   */\n  TIMEOUT = 'TIMEOUT',\n  /**\n   * HTTP status error.\n   */\n  HTTP = 'HTTP',\n  /**\n   * Request `options` is not valid. It's not a response error.\n   */\n  BAD_REQUEST = 'BAD_REQUEST',\n  /**\n   * `onAfterResponse` hook throws error.\n   */\n  BROKEN_ON_AFTER_RESPONSE = 'BROKEN_ON_AFTER_RESPONSE',\n  /**\n   * `onBeforeRequest` hook throws error.\n   */\n  BROKEN_ON_BEFORE_REQUEST = 'BROKEN_ON_BEFORE_REQUEST',\n  /**\n   * `onInit` hook throws error.\n   */\n  BROKEN_ON_INIT = 'BROKEN_ON_INIT',\n  /**\n   * `onResponseError` hook throws error.\n   */\n  BROKEN_ON_RESPONSE_ERROR = 'BROKEN_ON_RESPONSE_ERROR',\n  /**\n   * `onNonResponseError` hook throws error.\n   */\n  BROKEN_ON_NON_RESPONSE_ERROR = 'BROKEN_ON_NON_RESPONSE_ERROR',\n  /**\n   * `onHeadersReceived` hook throws error.\n   */\n  BROKEN_ON_HEADERS_RECEIVED = 'BROKEN_ON_HEADERS_RECEIVED',\n  /**\n   * Lyla instance created with `withRetry` throws an unexpected error. This\n   * error isn't created by `lyla` instance itself, but thrown by `onRejected`\n   * or `onResolved` of `withRetry` or the process of creating retry request options\n   * defined by user.\n   *\n   * The error won't be created by `lyla` instance that not created with `withRetry`.\n   */\n  BROKEN_RETRY = 'BROKEN_RETRY',\n  /**\n   * A non-lyla error is return by `onRejected` or `onResolved`'s `reject` action.\n   * Lyla error won't be wrapped in this error.\n   *\n   * The error won't be created by `lyla` instance that not created with `withRetry`.\n   */\n  RETRY_REJECTED_BY_NON_LYLA_ERROR = 'RETRY_REJECTED_BY_NON_LYLA_ERROR'\n}\n```\n\n### Global error listener\n\n```ts\nimport { createLyla } from '@lylajs/web'\n\nconst { lyla } = createLyla({\n  context: null,\n  hooks: {\n    onResponseError(error) {\n      switch error.type {\n        // ...\n      }\n    },\n    onNonResponseError(error) {\n       switch error.type {\n        // ...\n      }\n    }\n  }\n})\n```\n\n## Aborting Request\n\nYou can use native `AbortController` or `LylaAbortController` to abort requests.\n\nPlease note that `LylaAbortController` doesn't polyfill all APIs of\n`AbortController`.\n\n```ts\nimport { createLyla, LylaAbortController } from '@lylajs/web'\n\nconst controller = new LylaAbortController()\n\nconst { lyla } = createLyla({ context: null })\n\nlyla.get('url', {\n  signal: controller.signal\n})\n\ncontroller.abort()\n```\n\n## Context\n\nYou can access a context object in hooks, responses \u0026 errors.\n\n```ts\nconst { lyla, isLylaError } = createLyla({\n  context: {\n    startTime: -1,\n    endTime: -1,\n    duration: -1\n  },\n  hooks: {\n    onInit: [\n      (options) =\u003e {\n        options.context.startTime = Date.now()\n        return options\n      }\n    ],\n    onResponseError: [\n      (options) =\u003e {\n        options.context.endTime = Date.now()\n        options.context.duration =\n          options.context.endTime - options.context.startTime\n        return options\n      }\n    ],\n    onAfterResponse: [\n      (options) =\u003e {\n        options.context.endTime = Date.now()\n        options.context.duration =\n          options.context.endTime - options.context.startTime\n        return options\n      }\n    ]\n  }\n})\n\nlyla.get('/foo').then((response) =\u003e {\n  console.log(response.context.duration)\n})\n```\n\n## Retry request\n\nBeta compatibility. Currently only work in `2.0.0-beta.1` and later versions.\n\nLyla provides a `withRetry` method to create a lyla instance with retry capability.\n\n`lyla.withRetry(options: LylaWithRetryOptions) =\u003e Lyla`\n\nThe type `LylaWithRetryOptions` is (simplified version):\n\n```ts\ntype LylaWithRetryOptions\u003cS\u003e = {\n  onResolved: (params: {\n    state: S\n    options: LylaRequestOptions\n    response: LylaResponse\n  }) =\u003e Promise\u003c\n    | {\n        action: 'retry'\n        value: () =\u003e Promise\u003cLylaRequestOptions\u003e | LylaRequestOptions\n      }\n    | {\n        action: 'resolve'\n        value: LylaResponse\n      }\n    | {\n        action: 'reject'\n        value: unknown\n      }\n  \u003e\n  onRejected: (params: {\n    state: S\n    options: LylaRequestOptionsWithContext\n    lyla: Lyla\n    error: LylaError\n  }) =\u003e Promise\u003c\n    | {\n        action: 'retry'\n        value: () =\u003e Promise\u003cLylaRequestOptions\u003e | LylaRequestOptions\n      }\n    | {\n        action: 'reject'\n        value: unknown\n      }\n  \u003e\n  createState: () =\u003e S\n}\n```\n\nThe `lyla.withRetry` method returns a new lyla instance with all lyla methods except `retry`. This instance will decide whether to retry, continue, or reject based on the return values of `onResolved` and `onRejected`.\n\n`createState` is called at the beginning of each request to create a new state object, which is passed to `onResolved` and `onRejected`. You can use this object to store some state, such as retry count.\n\nHere is an example of retrying three times:\n\n```ts\nimport { createLyla } from '@lylajs/*' // * is the platform you need\n\nconst { lyla: _lyla } = createLyla({ context: null })\n\nconst lyla = _lyla.withRetry({\n  createState: () =\u003e ({\n    count: 0\n  }),\n  onResolved: async ({ response }) =\u003e {\n    return {\n      action: 'resolve',\n      value: response\n    }\n  },\n  onRejected: async ({ state, error, options }) =\u003e {\n    state.count += 1\n    if (state.count \u003e 3) {\n      return {\n        action: 'reject',\n        value: error\n      }\n    } else {\n      return {\n        action: 'retry',\n        // Retry with the original options\n        value: () =\u003e options\n      }\n    }\n  }\n})\n```\n\nAn instance created by `lyla.withRetry` may encounter three types of errors when sending a request:\n1. A Lyla Error returned by the reject action, which will be thrown directly without being wrapped.\n2. A non-Lyla Error (e.g., `error1`) returned by the reject action, which will be wrapped into a `RETRY_REJECTED_BY_NON_LYLA_ERROR` type error and thrown (e.g., `error2`). `error1` can be accessed via `error2.error`.\n3. An exception thrown by `onResolved` or `onRejected`, or an exception thrown by the value function of the retry action, which will be wrapped into a `BROKEN_RETRY` type error and thrown.\n4. Retry may throw 2 unique errors, `RETRY_REJECTED_BY_NON_LYLA_ERROR` and `BROKEN_RETRY`, which won't be caught by any hook and won't be judged as `true` by `isLylaError`. If you need to judge these two errors, you need to use `isLylaErrorWithRetry`, which will judge all Lyla Errors.\n\n## FAQ\n\n- Why not axios？\n  - `axios.defaults` will be applied to all axios instances created by `axios.create`, which means your code may be influenced unexpectedly by others. The behavior can't be changed by any options.\n  - `axios.defaults` is a global singleton, which means you can't get a clean copy of it. Since your code may run after than others' code that modifies it.\n  - axios will transform invalid JSON to string sliently by default.\n  - axios can't access an typed context object in request processes.\n- Why not ky？\n  - ky is based on fetch, it can't support upload progress.\n  - ky is based on fetch, it can't access headers before response is fully resolved.\n  - ky's Response data type can't be typed.\n  - ky can't access an typed context object in request processes.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F07akioni%2Flyla","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F07akioni%2Flyla","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F07akioni%2Flyla/lists"}