{"id":25776058,"url":"https://github.com/Nike-Inc/backstopper","last_synced_at":"2025-02-27T06:05:27.985Z","repository":{"id":11204957,"uuid":"68253152","full_name":"Nike-Inc/backstopper","owner":"Nike-Inc","description":"Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and up.","archived":false,"fork":false,"pushed_at":"2024-09-15T20:57:59.000Z","size":1485,"stargazers_count":42,"open_issues_count":4,"forks_count":22,"subscribers_count":22,"default_branch":"main","last_synced_at":"2025-02-23T08:11:24.196Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nike-Inc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","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":"2016-09-15T00:01:17.000Z","updated_at":"2025-02-11T01:27:01.000Z","dependencies_parsed_at":"2024-12-17T18:11:25.443Z","dependency_job_id":"b4a90fe6-7fbc-4150-88fc-88f2edd28a0e","html_url":"https://github.com/Nike-Inc/backstopper","commit_stats":null,"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nike-Inc%2Fbackstopper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nike-Inc%2Fbackstopper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nike-Inc%2Fbackstopper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nike-Inc%2Fbackstopper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nike-Inc","download_url":"https://codeload.github.com/Nike-Inc/backstopper/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240987435,"owners_count":19889333,"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":[],"created_at":"2025-02-27T06:01:16.942Z","updated_at":"2025-02-27T06:05:27.972Z","avatar_url":"https://github.com/Nike-Inc.png","language":"Java","funding_links":[],"categories":["REST错误处理"],"sub_categories":[],"readme":"\u003cimg src=\"backstopper_logo.png\" /\u003e\n\n# Backstopper - Keep Your API Errors in the Field of Play\n\n[![Maven Central][maven_central_img]][maven_central]\n[![Build][gh_action_build_img]][gh_action_build]\n[![Code Coverage][codecov_img]][codecov]\n[![License][license img]][license]\n\n**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and\ngreater.**\n\n(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of\nBackstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta`\necosystem. The Backstopper 1.x releases also contain support for JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and \nSpringboot 1 and 2 - see \n[here](https://github.com/Nike-Inc/backstopper/tree/v1.x?tab=readme-ov-file#framework_modules).)\n\n## TL;DR\n\nBackstopper guarantees that a consistent error contract which you can define will be sent to callers *no matter the\nsource of the error* when properly integrated into your API framework. No chance of undesired information leaking to the\ncaller like stack traces, and callers can always rely on an error contract they can programmatically process.\n\nIf you prefer hands-on exploration rather than readmes and user guides, the [sample applications](#samples) provide\nconcrete examples of using Backstopper that are simple, compact, and straightforward.\n\nThe rest of this readme is intended to get you oriented and running quickly, and the [User Guide](USER_GUIDE.md)\ncontains more in-depth details.\n\n\u003ca name=\"table_of_contents\"\u003e\u003c/a\u003e\n\n## Table of Contents\n\n\u003c!-- TOC --\u003e\n* [Additional Features Overview](#overview)\n* [Barebones Example](#barebones_example)\n* [Quickstart](#quickstart)\n  * [Quickstart - Integration](#quickstart_integration)\n  * [Quickstart - Usage](#quickstart_usage)\n    * [Defining a set of API errors](#quickstart_usage_api_error_enum)\n    * [Defining a ProjectApiErrors for your project](#quickstart_usage_project_api_errors)\n    * [Manually throwing an error](#quickstart_usage_throw_api_exception)\n    * [Creating a custom exception listener](#quickstart_usage_add_custom_listener)\n* [Integration Modules](#modules)\n  * [General-Purpose Modules](#general_modules)\n  * [Framework-Specific Modules](#framework_modules)\n* [Sample Applications](#samples)\n* [User Guide](USER_GUIDE.md)\n* [License](#license)\n\u003c!-- TOC --\u003e\n\n\u003ca name=\"overview\"\u003e\u003c/a\u003e\n\n## Additional Features Overview\n\n* Backstopper trivializes the task of creating and throwing errors that will be sent to the caller with the desired\n  error contract, and allows you to gather all of your project-specific API error definitions in one place (e.g. as an\n  enum) for easy access, modification, and addition.\n* All errors are logged with full request context and error debugging info along with a UUID. The caller is sent the\n  same UUID in the error response payload so you can instantly connect an individual error response with the application\n  log message containing full debugging details. This makes the usual tedious process of debugging why an API error was\n  shown to the caller simple and straightforward.\n* Includes optional support for defining a common set of \"core API errors\" that can be shared via jar library so that\n  all projects in your organization use the same set of API errors for common error cases. Individual projects are then\n  free to focus on their project-specific API errors.\n* Contains optional integration support for Java's JSR 303 Bean Validation system (`@NotNull`, `@Max`, etc) so that\n  validation errors occurring due to invalid payloads sent by callers (among other use cases) will be automatically\n  converted to your predefined set of project API errors.\n* Integration libraries are included for several API frameworks already, and adding support for other frameworks is a\n  relatively straightforward process. Backstopper is geared primarily towards HTTP-based APIs, but could be used in\n  other circumstances if desired and without polluting your project with unwanted HTTP API dependencies.\n\n\u003ca name=\"barebones_example\"\u003e\u003c/a\u003e\n\n## Barebones Example (assumes [framework integration](#quickstart_integration) is already done)\n\n##### 1. Define your API's errors\n\n``` java\npublic enum MyProjectError implements ApiError {\n    // Constructor args for this example are: errorCode, message, httpStatusCode\n    EMAIL_CANNOT_BE_EMPTY(42, \"Email must be specified\", 400),\n    INVALID_EMAIL_ADDRESS(43, \"Invalid email address format\", 400),\n    // -- SNIP -- \n}\n```\n\n##### 2a. Use JSR 303 Bean Validation to define object validation (optional)\n\n``` java\npublic class Payload {\n    @NotEmpty(message = \"EMAIL_CANNOT_BE_EMPTY\")\n    @Email(message = \"INVALID_EMAIL_ADDRESS\")\n    private String email;\n    // -- SNIP -- \n}\n```\n\n##### --AND/OR--\n\n##### 2b. Throw errors manually anytime (doesn't have to be just for validation)\n\n``` java\nthrow ApiException.newBuilder()\n                  .withApiErrors(MyProjectError.INVALID_EMAIL_ADDRESS)\n                  .withExceptionMessage(\"Error validating email field.\")\n                  .withExtraResponseHeaders(Pair.of(\"received-email\", singletonList(payload.email)))\n                  .withExtraDetailsForLogging(Pair.of(\"received_email\", payload.email))\n                  .build();\n```  \n\n##### 3. Send payload that will cause errors to be thrown\n\n`POST /profile`\n\n``` json\n{\n\t\"email\": \"not@a@valid@email\",\n\t\"etc\": \"-- SNIP --\"\n}\n```\n\n##### 4. Receive expected error response\n\n`HTTP Status Code: 400 Bad Request`\n\n``` json\n{\n  \"error_id\": \"408d516f-68d1-41f3-adb1-b3dc0affaaf2\",\n  \"errors\": [\n    {\n      \"code\": 43,\n      \"message\": \"Invalid email address format\",\n      \"metadata\": {\n        \"field\": \"email\"\n      }\n    }\n  ]\n}\n```\n\n##### 5. Use the error_id to locate debugging details in the logs (a 5xx error would also include the stack trace)\n\n```\n2016-09-21_12:14:00.620 |-WARN  c.n.b.h.s.SpringApiExceptionHandler - ApiExceptionHandlerBase handled \n↪exception occurred: error_uid=408d516f-68d1-41f3-adb1-b3dc0affaaf2, \n↪exception_class=com.nike.backstopper.exception.ClientDataValidationError, returned_http_status_code=400, \n↪contributing_errors=\"INVALID_EMAIL_ADDRESS\", request_uri=\"/profile\", request_method=\"POST\", \n↪query_string=\"null\", request_headers=\"Host=localhost:8080, --snip-- Content-Type=application/json\",\n↪client_data_validation_failed_objects=\"com.myorg.Payload\", \n↪constraint_violation_details=\"Payload.email|org.hibernate.validator.constraints.Email|INVALID_EMAIL_ADDRESS\"\n```\n\n\u003ca name=\"quickstart\"\u003e\u003c/a\u003e\n\n## Quickstart\n\nGetting started is a matter of integrating Backstopper into your project and learning how to use its features. The\nfollowing sections will help guide you in getting started, and the [sample applications](#samples) should be consulted\nfor concrete examples.\n\n\u003ca name=\"quickstart_integration\"\u003e\u003c/a\u003e\n\n### Quickstart - Integration\n\nThe first thing to do is see if there is a [framework integration plugin library](#framework_modules) already created\nfor the framework you're using. If so then refer to that framework-specific library's readme as well as\nits [sample project](#samples) to learn how to integrate Backstopper into your project.\n\nIf a framework-specific plugin library does not already exist for your project then you'll need to create your own\nintegration. If the result is potentially reusable for others using the same framework then please\nconsider [contributing](CONTRIBUTING.md) it back to the Backstopper project so others can benefit!\nThe [new framework integrations](USER_GUIDE.md#new_framework_integrations) section has full details, and in particular\nthe [pseudo-code section](USER_GUIDE.md#new_framework_pseudocode) should give you a quick idea of what is required.\n\n**IMPORTANT NOTE: Your project integration should not be considered complete until you have added and enabled the\nreusable unit tests that enforce Backstopper rules and conventions.** See\nthe [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](USER_GUIDE.md#reusable_tests) section of the\nUser Guide for information on setting these up.\n\n\u003ca name=\"quickstart_usage\"\u003e\u003c/a\u003e\n\n### Quickstart - Usage\n\nOnce your project is properly integrated with Backstopper a large portion of errors should be handled for you (framework\nerrors, errors resulting from validation of incoming payloads, etc), however for most API projects you'll need to throw\nerrors or interact with Backstopper in other situations. Again, the [sample projects](#samples) are excellent for\nshowing how this is done in practice, but here are a few common use cases and how to solve them:\n\n\u003ca name=\"quickstart_usage_api_error_enum\"\u003e\u003c/a\u003e\n\n##### Defining a set of `ApiError`s\n\nDefining groups of `ApiError`s as enums has proven to be a useful pattern. Normally you'd want to break `ApiError`s out\ninto a group of \"core errors\" that you could share with projects across your organization (see `SampleCoreApiError` for\nan example) and different sets of `ApiError`s for each individual project (see any of the [sample application](#samples)\n`SampleProjectApiError` classes for an example). For the purpose of this example here is a mishmash showing how to\ndefine an enum of errors with different properties (basic code/message/http-status errors, \"mirror\" errors, and errors\nwith metadata):\n\n``` java\npublic enum MyProjectApiError implements ApiError {\n    GENERIC_SERVICE_ERROR(10, \"An error occurred while fulfilling the request\", 500),\n    // Mirrors GENERIC_SERVICE_ERROR for the caller, but will show up in the logs with a different name\n    SOME_OTHER_SERVICE_ERROR(GENERIC_SERVICE_ERROR),\n    GENERIC_BAD_REQUEST(20, \"Invalid request\", 400),\n    // Includes metadata in the response payload sent to the caller\n    SOME_OTHER_BAD_REQUEST(30, \"You failed to pass the required foo\", 400, Map.of(\"missing_field\", \"foo\")),\n    // Also a mirror for another ApiError, but includes extra metadata that will show up in the response\n    YET_ANOTHER_BAD_REQUEST(GENERIC_BAD_REQUEST, Map.of(\"field\", \"bar\"));\n\n    private final ApiError delegate;\n\n    MyProjectApiError(ApiError delegate) {this.delegate = delegate; }\n\n    MyProjectApiError(ApiError delegate, Map\u003cString, Object\u003e additionalMetadata) {\n        this(new ApiErrorWithMetadata(delegate, additionalMetadata));\n    }\n\n    MyProjectApiError(int errorCode, String message, int httpStatusCode) {\n        this(errorCode, message, httpStatusCode, null);\n    }\n\n    MyProjectApiError(int errorCode, String message, int httpStatusCode, Map\u003cString, Object\u003e metadata) {\n        this(new ApiErrorBase(\n            \"delegated-to-enum-name-\" + UUID.randomUUID(), errorCode, message, httpStatusCode,\n            metadata\n        ));\n    }\n\n    @Override\n    public String getName() { return this.name(); }\n\n    @Override\n    public String getErrorCode() { return delegate.getErrorCode(); }\n\n    @Override\n    public String getMessage() { return delegate.getMessage(); }\n\n    @Override\n    public int getHttpStatusCode() { return delegate.getHttpStatusCode(); }\n\n    @Override\n    public Map\u003cString, Object\u003e getMetadata() { return delegate.getMetadata(); }\n\n}\n```\n\n\u003ca name=\"quickstart_usage_project_api_errors\"\u003e\u003c/a\u003e\n\n##### Defining a `ProjectApiErrors` for your project\n\nBackstopper needs a `ProjectApiErrors` defined for each project in order to work. If possible you should create an\nabstract base class that is setup with the core errors for your organization - see `SampleProjectApiErrorsBase` for an\nexample. Then each individual project would simply need to extend the base class and fill in the project-specific set of\n`ApiError`s and the error range it's using. See any of the [sample application](#samples) `SampleProjectApiErrorsImpl`\nclasses for an example. The javadocs for `ProjectApiErrors` contains in-depth information as well.\n\n\u003ca name=\"quickstart_usage_throw_api_exception\"\u003e\u003c/a\u003e\n\n##### Manually throwing an arbitrary error with full control over the resulting error contract, response headers, and logging info\n\n``` java\n// The only requirement is that you include at least one ApiError. Everything else is optional.\nthrow ApiException.newBuilder()\n                  .withApiErrors(MyProjectApiError.FOO_ERROR, MyProjectApiError.BAD_THING_HAPPENED)\n                  .withExceptionMessage(\"Useful message for exception in the logs\")\n                  .withExceptionCause(originalCause)\n                  .withExtraResponseHeaders(\n                      Pair.of(\"useful-single-header-for-caller\", singletonList(\"thing1\")),\n                      Pair.of(\"also-useful-multivalue-header\", Arrays.asList(\"thing2\", \"thing3\"))\n                  )\n                  .withExtraDetailsForLogging(\n                      Pair.of(\"important_info\", \"foo\"),\n                      Pair.of(\"also_important\", \"bar\")\n                  )\n                  .build();\n```\n\nAbove is a \"kitchen sink\" example showing all the options available when throwing an `ApiException` manually. For \nautomatic input validation and error handling see the User Guide's section on \n[Bean Validation](USER_GUIDE.md#jsr_303_support).\n\n\u003ca name=\"quickstart_usage_add_custom_listener\"\u003e\u003c/a\u003e\n\n##### Creating a custom `ApiExceptionHandlerListener` to handle a typed exception\n\nThis is only really necessary if you can't (or don't want to) throw an `ApiException` and need Backstopper to properly\nhandle a typed exception it wouldn't otherwise know about. Many projects never need to do this.\n\n``` java\npublic class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener {\n    @Override\n    public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) {\n        if (ex instanceof MyFrameworkException myEx) {\n            // The exception is a MyFrameworkException, so this listener should handle it.\n            SortedApiErrorSet apiErrors =\n                SortedApiErrorSet.singletonSortedSetOf(MyProjectApiError.SOME_OTHER_BAD_REQUEST);\n            List\u003cPair\u003cString, String\u003e\u003e extraDetailsForLogging = Arrays.asList(\n                Pair.of(\"important_foo_info\", myEx.foo()),\n                Pair.of(\"important_bar_info\", myEx.bar())\n            );\n            List\u003cPair\u003cString, List\u003cString\u003e\u003e\u003e extraResponseHeaders = Arrays.asList(\n                Pair.of(\"foo-info\", singletonList(myEx.foo())),\n                Pair.of(\"bar-info\", singletonList(myEx.bar()))\n            );\n            return ApiExceptionHandlerListenerResult.handleResponse(\n                apiErrors, extraDetailsForLogging, extraResponseHeaders\n            );\n        }\n\n        // The exception wasn't a MyFrameworkException, so this listener should ignore it.\n        return ApiExceptionHandlerListenerResult.ignoreResponse();\n    }\n}\n```\n\nAfter defining a new `ApiExceptionHandlerListener` you'll need to register it with the `ApiExceptionHandlerBase` running\nyour Backstopper system. This is a procedure that is often different for each framework integration.\n\n\u003ca name=\"modules\"\u003e\u003c/a\u003e\n\n## Integration Modules\n\n\u003ca name=\"general_modules\"\u003e\u003c/a\u003e\n\n### General-Purpose Modules\n\n* [backstopper-core](backstopper-core/) - The core library providing the majority of the Backstopper functionality.\n* [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5/) - There are some rules around defining your\n  project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be\n  followed for Backstopper to function at its best. This library contains reusable tests that are intended to be\n  included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the\n  build with descriptive unit test errors when those rules are violated. These are technically optional but ***highly***\n  recommended - integration is generally quick and easy.\n* [backstopper-custom-validators](backstopper-custom-validators/) - This library contains JSR 303 Bean Validation\n  annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency\n  free so this library is usable in non-Backstopper projects that utilize JSR 303 validations.\n* [backstopper-jackson](backstopper-jackson/) - Contains a few utilities that help integrate Backstopper and Jackson for\n  serializing error contracts to JSON. Optional.\n* [backstopper-servlet-api](backstopper-servlet-api/) - Intermediate library intended to ease integration with\n  Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we\n  don't already have support for then you'll want to use this.\n* [backstopper-spring-web](backstopper-spring-web/) - Intermediate library intended to ease integration with\n  Spring-based frameworks. If you're building a Spring integration library for a Spring-based framework that we\n  don't already have support for then you'll want to use this.\n* [nike-internal-util](nike-internal-util/) - A small utilities library that provides some reusable helper methods and\n  classes. It is \"internal\" in the sense that it is not intended to be directly pulled in and used by non-Nike projects.\n  That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers,\n  backwards compatibility, etc over time when compared with libraries specifically intended for public consumption.\n\n\u003ca name=\"framework_modules\"\u003e\u003c/a\u003e\n\n### Framework-Specific Modules\n\n* [backstopper-spring-web-mvc](backstopper-spring-web-mvc/) - Base Integration library for the Spring Web MVC (Servlet)\n  framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for\n  Spring 6+, and used as a foundation for Backstopper support in Spring Boot 3 MVC (see below for links to Spring\n  Boot specific integration modules). There is a [Spring Web MVC sample project](samples/sample-spring-web-mvc/)\n  complete with integration tests you can use as an example.\n* [backstopper-spring-web-flux](backstopper-spring-web-flux/) - Integration library for the Spring WebFlux (Netty)\n  framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a\n  [Spring Boot 3 WebFlux sample project](samples/sample-spring-boot3-webflux/) complete with integration tests you can\n  use as an example.\n* [backstopper-spring-boot3-webmvc](backstopper-spring-boot3-webmvc/) - Integration library for the Spring Boot 3\n  framework *if and only if you're using the Spring Web MVC Servlet runtime* (if you're running a Spring\n  Boot 3 + WebFlux Netty application, then you do not want this library and should use\n  [backstopper-spring-web-flux](backstopper-spring-web-flux) instead). If you want to integrate Backstopper into a\n  project running in Spring Boot 3 + Web MVC then start here. There is a\n  [Spring Boot 3 Web MVC sample project](samples/sample-spring-boot3-webmvc/) complete with integration tests you\n  can use as an example.\n\nNOTE: Backstopper 1.x releases contain support for the `javax` ecosystem, JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and\nSpringboot 1 and 2 - see\n[here](https://github.com/Nike-Inc/backstopper/tree/v1.x?tab=readme-ov-file#framework_modules) if you need support\nfor these older frameworks.\n\nWe have not created support for `jakarta` based JAX-RS or Jersey frameworks in Backstopper 2.x due to lack of\ninterest, but it could be done.\n\n\u003ca name=\"samples\"\u003e\u003c/a\u003e\n\n## Framework Integration Sample Applications\n\nThe sample apps are an excellent source for framework integration examples, but they are also very helpful for\ngiving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw\nerrors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors\noccur, and how to find the relevant log message given a specific error response. The\n`VerifyExpectedErrorsAreReturnedComponentTest` component tests in the sample apps exercise a large portion of\nBackstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in\nthe error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it\nall fits together.\n\n* [samples/sample-spring-web-mvc](samples/sample-spring-web-mvc/)\n* [samples/sample-spring-boot3-webmvc](samples/sample-spring-boot3-webmvc/)\n* [samples/sample-spring-boot3-webflux](samples/sample-spring-boot3-webflux/)\n\n## User Guide\n\nFor further details please consult the [User Guide](USER_GUIDE.md).\n\n\u003ca name=\"license\"\u003e\u003c/a\u003e\n\n## License\n\nBackstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)\n\n[maven_central]:https://search.maven.org/search?q=g:com.nike.backstopper\n[maven_central_img]:https://maven-badges.herokuapp.com/maven-central/com.nike.backstopper/backstopper-core/badge.svg?style=flat\n\n[gh_action_build]:https://github.com/Nike-Inc/backstopper/actions/workflows/build.yml\n[gh_action_build_img]:https://github.com/Nike-Inc/backstopper/actions/workflows/build.yml/badge.svg\n\n[codecov]:https://codecov.io/github/Nike-Inc/backstopper\n[codecov_img]:https://codecov.io/github/Nike-Inc/backstopper/branch/main/graph/badge.svg?token=zLHgFMsJpj\n\n[license]:LICENSE.txt\n[license img]:https://img.shields.io/badge/License-Apache%202-blue.svg\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNike-Inc%2Fbackstopper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FNike-Inc%2Fbackstopper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FNike-Inc%2Fbackstopper/lists"}