{"id":24110277,"url":"https://github.com/orlando-pt/github-repos","last_synced_at":"2026-04-06T21:31:49.579Z","repository":{"id":217020732,"uuid":"742725503","full_name":"Orlando-pt/github-repos","owner":"Orlando-pt","description":"challenging challenges","archived":false,"fork":false,"pushed_at":"2024-02-12T16:16:36.000Z","size":231,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-28T11:45:58.296Z","etag":null,"topics":["aws","cdk","docker","jenkins","kotlin","reactive","webflux"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","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/Orlando-pt.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":"2024-01-13T07:11:34.000Z","updated_at":"2024-08-08T01:26:14.000Z","dependencies_parsed_at":"2025-02-28T05:21:43.126Z","dependency_job_id":null,"html_url":"https://github.com/Orlando-pt/github-repos","commit_stats":null,"previous_names":["orlando-pt/github-repos"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Orlando-pt/github-repos","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Orlando-pt%2Fgithub-repos","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Orlando-pt%2Fgithub-repos/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Orlando-pt%2Fgithub-repos/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Orlando-pt%2Fgithub-repos/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Orlando-pt","download_url":"https://codeload.github.com/Orlando-pt/github-repos/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Orlando-pt%2Fgithub-repos/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31491096,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T17:22:55.647Z","status":"ssl_error","status_checked_at":"2026-04-06T17:22:54.741Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["aws","cdk","docker","jenkins","kotlin","reactive","webflux"],"created_at":"2025-01-11T01:13:46.086Z","updated_at":"2026-04-06T21:31:49.549Z","avatar_url":"https://github.com/Orlando-pt.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Reactive Challenge\n\nHi! In this file you will find the most important information about a challenge resolution described in\nthis [file](./challenge.pdf).\n\n## Chosen technologies\n\nI choose to use [Spring Webflux](https://www.baeldung.com/spring-webflux) and\n[Kotlin coroutines](https://kotlinlang.org/docs/coroutines-overview.html), which provides reactive programming\nsupport for web applications. I created the Dockerfile\nto build the docker image, sending it to the AWS ECR to be used by the ECS.\nThe [Jenkinsfile](./Jenkinsfile) contains the description of the pipeline used to build, test and deploy\nthe application to AWS.\n\n## Application Development\n\nThe application only had one endpoint, so it was really simple to implement. If we think in an\nup-down approach, we start by the\n[RepositoryController.kt](./src/main/kotlin/com/tui/githubrepos/controller/RepositoryController.kt)\nwhere we define the endpoint and pass the request to the service layer. The\n[RepositoryService.kt](./src/main/kotlin/com/tui/githubrepos/service/RepositoryService.kt) is then\nresponsible for calling a GitHub client wrapper(\n[GithubClient.kt](./src/main/kotlin/com/tui/githubrepos/httpclient/GithubClient.kt)\n) that will call the GitHub API and retrieve the data. **Coroutines** allows us to do this in a non-blocking\nway, so we can have better performance.\n\nTo run the application locally, have in mind that you need to have a **GITHUB_TOKEN** to be able to\ncall the GitHub API. I had a **.env.local** file with the environment variables.\n\n```shell\n$ source .env.local\n$ ./gradlew bootRun\n```\n\nI tested the service layer using **unit tests** available at\n[RepositoryServiceTest.kt](./src/test/kotlin/com/tui/githubrepos/service/RepositoryServiceTest.kt).\nThe main purpose of these tests is to check if the service logic is working as expected. It is using\n**Mockito** to mock the response from the *GitHub client* and also **JUnit** to run the tests.\nIn terms of **integration tests**, I created the test class\n[RepositoryControllerTestIT.kt](./src/test/kotlin/com/tui/githubrepos/controller/RepositoryControllerTestIT.kt)\nthat is responsible for loading the whole application and test the endpoint.\nThe **GitHub** Http api is mocked using\n[MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver) from **OkHttp**.\nIn the file\n[RepositoryControllerMockTestIT.kt](./src/test/kotlin/com/tui/githubrepos/controller/RepositoryControllerMockTestIT.kt)\nI mock the service and test error handling at the controller level.\n\nNormally I have two different *gradle tasks* to run the tests, one for unit tests and another for\nintegration tests. But this time, to simplify, it's only one task.\n\n```shell\n$ ./gradlew test\n```\n\nThere was emphasis on how to deal when a GitHub user doesn't exist. I decided to create a custom\nexception that is thrown when the *http client* returns a *404* status code. The exception is then\ncaught by the following\n[ExceptionHandler.kt](./src/main/kotlin/com/tui/githubrepos/exception/handler/ExceptionHandler.kt).\nThe response is custom just like it was asked in the challenge description.\n\n```json\n{\n  \"status\": 404,\n  \"message\": \"Username not found: JohnDoe\"\n}\n```\n\nIn the case of the client trying to request data in *XML* format, I handled it by telling the\nendpoint to produce only *JSON* responses.\n\n```kotlin\n@GetMapping(\"/{username}\", produces = [MediaType.APPLICATION_JSON_VALUE])\n```\n\nIn reality, *Spring Webflux* doesn't support *XML* responses natively, there is some workarounds\nas we can see in the\nfollowing [link](https://stackoverflow.com/questions/55306194/springboot-webflux-cannot-return-application-xml).\nThe problem with these workarounds is that they are not intuitive, and when I implemented them they simply didn't work.\nWe have limited time to complete the challenge, so I decided that the following\nmessage was okay enough. I know the implications of having different formatted responses,\nspecially for the ones consuming the API, it's not good, and I would never deliver it like this.\n\n```shell\n$ curl -H \"Accept: application/xml\" https://.../api/repository/JohnDoe\n```\n\n```json\n{\n  \"timestamp\": \"2024-01-15T22:55:50.391+00:00\",\n  \"path\": \"/repository/JohnDoe\",\n  \"status\": 406,\n  \"error\": \"Not Acceptable\",\n  \"requestId\": \"68bf40e6-4\"\n}\n```\n\n## Code Quality \u0026 Standards\n\nThe code was written having in mind the best practices and clean code. I followed the\n[Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html) and the\ncomments are compliant with **JavaDoc**. The other practices are inside the code, and are better\nseen than explained.\n\n## Swagger\n\nThe **Swagger** documentation is generated automatically using a gradle task. I copied the generated\n[swagger file](./swagger.yaml) from the *build* folder for us to have an example.\n\n```shell\n$ ./gradlew generateOpenApiDocs\n```\n\nThis automatic generation can be useful later if we want to customize the **API Gateway**.\nWith the swagger file we know exactly which endpoints we have and what are the expected responses.\n\nSwagger also has a **UI** component that we can find after\ngenerating it automatically, when running the application.\n\n---\n\n## Docker\n\nThe [Dockerfile](./Dockerfile) is very simple, it just copies the **jar** file and runs it. I also\nadded comments to explain what we could do if we wanted also to build the jar. There is also the\noption of creating a native image, that basically uses [GraalVM](https://www.graalvm.org/) to\ncompile the application to a native image, but it was a little out of the context. I decided to only copy the jar and\nnot build it because we have the **Jenkins pipeline** for that.\n\nLocally, we can run and build the docker image with the following commands:\n\n```shell\n$ docker build -t github-repos:latest .\n$ docker run --env-file .env.local.docker -p 8090:8080 --name github-app github-repos\n```\n\n## AWS \u0026 CloudFormation\n\nI used the [AWS CDK](https://aws.amazon.com/cdk/) to create the infrastructure. It's a framework\nthat allows to create the infrastructure using code, in the end it generates a **CloudFormation**\ntemplate. It is a very simple way to create and manage resources in AWS as you can see in the\nstack file [infra-stack.ts](./infra/lib/infra-stack.ts).\n\nI started by creating the VPC.\n\n```typescript\nconst vpc = new Vpc(this, getNameWithEnv(\"GithubReposVpc\"), {\n    maxAzs: 2,\n    natGateways: 1,\n    restrictDefaultSecurityGroup: false,\n});\n```\n\nFollowed by the ECS cluster.\n\n```typescript\nconst appCluster = new ecs.Cluster(this, getNameWithEnv(\"GithubReposEcs\"), {\n    vpc: vpc,\n    clusterName: getNameWithEnv(\"GithubReposCluster\"),\n});\n```\n\nAnd then the Fargate service with the ALB.\n\n```typescript\nconst sbApp = new ApplicationLoadBalancedFargateService(\n    this,\n    getNameWithEnv(\"GithubReposApp\"),\n    {\n        cluster: appCluster,\n        desiredCount: 1,\n        cpu: 256,\n        memoryLimitMiB: 512,\n        taskImageOptions: {\n            image: ecs.ContainerImage.fromAsset(\"..\"),\n            containerPort: 8080,\n            secrets: {\n                GITHUB_TOKEN: ecs.Secret.fromSecretsManager(\n                    appSecrets,\n                    \"githubToken\"\n                ),\n            },\n        },\n        assignPublicIp: false,\n        publicLoadBalancer: false,\n    }\n);\n```\n\nLastly, I just created the Rest Api Gateway to connect to the ALB and expose the application.\n\n```typescript\nconst api = new HttpApi(this, getNameWithEnv(\"GithubReposApi\"));\n\napi.addRoutes({\n    path: \"/{proxy+}\",\n    methods: [HttpMethod.ANY],\n    integration: new HttpAlbIntegration(\n        getNameWithEnv(\"GithubReposAppIntegration\"),\n        sbApp.listener\n    ),\n});\n```\n\n## Jenkins\n\nThe following [Jenkinsfile](./Jenkinsfile) describes the pipeline used to build, test\nand deploy the application. I will also describe the steps here.\n\nStarting by building the application.\n\n```groovy\nstage('Build') {\n    steps {\n        sh './gradlew build -x test'\n    }\n}\n```\n\nThen, we run the tests. If I didn't have cheated with the tests, I would have two different tasks\nto run, unit and integration tests.\n\n```groovy\nstage('Test') {\n    steps {\n        sh './gradlew test'\n    }\n}\n```\n\nAfter tests, it's usually a good idea to\nuse [SonarQube](https://www.sonarsource.com/products/sonarqube/downloads/lts/8-9-lts/) to analyze the\ncode for bugs, vulnerabilities, smells, coverage, etc. I delayed that to another time, instead I deployed the\napplication to AWS.\n\n```groovy\nstage('Deploy') {\n    dir('infra') {\n        sh 'npm install'\n        sh 'npx cdk deploy --require-approval never'\n    }\n}\n```\n\nIt is also good to point out that we can generate the OpenAPI documentation before this stage\nand use it to customize the API Gateway.\n\n## Final considerations\n\nI had a lot of fun doing this challenge. I hope you learned a little bit about me and my work.\nBye, have a good day!\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forlando-pt%2Fgithub-repos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Forlando-pt%2Fgithub-repos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Forlando-pt%2Fgithub-repos/lists"}