{"id":17543585,"url":"https://github.com/lopcode/pellet","last_synced_at":"2025-06-25T17:03:11.706Z","repository":{"id":39789622,"uuid":"395832386","full_name":"lopcode/pellet","owner":"lopcode","description":"An opinionated, Kotlin-first web framework that helps you write fast, concise, and correct backend services 🚀.","archived":false,"fork":false,"pushed_at":"2024-07-01T04:03:41.000Z","size":5498,"stargazers_count":35,"open_issues_count":12,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-12-13T00:29:45.602Z","etag":null,"topics":["backend","coroutines","framework","kotlin","server-side","web"],"latest_commit_sha":null,"homepage":"https://www.pellet.dev","language":"Kotlin","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/lopcode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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":"2021-08-13T23:54:27.000Z","updated_at":"2024-04-23T19:38:12.000Z","dependencies_parsed_at":"2024-04-29T04:32:00.398Z","dependency_job_id":"8b1be9fb-0cec-470a-8116-e09da2381fc9","html_url":"https://github.com/lopcode/pellet","commit_stats":null,"previous_names":["lopcode/pellet","carrotcodes/pellet"],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopcode%2Fpellet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopcode%2Fpellet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopcode%2Fpellet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopcode%2Fpellet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lopcode","download_url":"https://codeload.github.com/lopcode/pellet/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230431103,"owners_count":18224655,"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":["backend","coroutines","framework","kotlin","server-side","web"],"created_at":"2024-10-21T00:24:53.879Z","updated_at":"2024-12-19T12:10:35.552Z","avatar_url":"https://github.com/lopcode.png","language":"Kotlin","readme":"# Pellet\n\n[![Maven Central](https://img.shields.io/maven-central/v/dev.pellet/pellet-server?style=flat-square)](https://mvnrepository.com/artifact/dev.pellet)\n[![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat-square)](http://www.apache.org/licenses/LICENSE-2.0)\n\nPellet is an opinionated Kotlin-first web framework that helps you write fast, concise, and correct backend services 🚀.\n\nPellet handles a huge number of requests per second, has a tiny dependency graph (`kotlin-stdlib`, `kotlinx.io`, and `slf4j-api`), and offers approximately one way of doing things.\nThe framework's conciseness is achieved through functional composition, instead of traditional JVM approaches involving annotations and reflection.\n\nI write about building Pellet in a series on my blog: https://www.lopcode.com/series/pellet/\n\n## Releases\n\nThis project is still in the prototyping phase - give the latest version a try! Let me know what you think, and what to focus on next, in [GitHub Discussions](https://github.com/lopcode/Pellet/discussions/categories/feedback) 💬.\n\nNote that the prototype is built with the latest JVM LTS release at the time of writing - Java 21.\n\nGradle (Kotlin):\n```\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    implementation(platform(\"dev.pellet:pellet-bom:0.0.16\"))\n    implementation(\"dev.pellet:pellet-server\")\n    implementation(\"dev.pellet:pellet-logging\")\n}\n```\n\n## Examples\n\nStarting a simple server, with one HTTP connector, and one route handler at `GET /v1/hello`, which responds with a 204:\n\n```kotlin\nfun main() = runBlocking {\n    val pellet = pelletServer {\n        httpConnector {\n            endpoint = PelletConnector.Endpoint(\n                hostname = \"localhost\",\n                port = 8082\n            )\n            router {\n                get(\"/v1/hello\") {\n                    HTTPRouteResponse.Builder()\n                        .noContent()\n                        .header(\"X-Hello\", \"World\")\n                        .build()\n                }\n            }\n        }\n    }\n    pellet.start()\n}\n```\n\nPellet will start up, and output log messages in a structured format:\n```\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.653728Z\",\"message\":\"Pellet server starting...\",\"name\":\"dev.pellet.server.PelletServer\",\"thread\":\"main\"}\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.748977Z\",\"message\":\"Get help, give feedback, and support development at https://www.pellet.dev\",\"name\":\"dev.pellet.server.PelletServer\",\"thread\":\"main\"}\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.749632Z\",\"message\":\"Starting connector: HTTP(hostname=localhost, port=8082, router=dev.pellet.server.routing.http.PelletHTTPRouter@189cbd7c)\",\"name\":\"dev.pellet.server.PelletServer\",\"thread\":\"main\"}\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.750098Z\",\"message\":\"Routes: \\nPelletHTTPRoute(method=GET, uri=/, handler=dev.pellet.demo.DemoKt$main$1$sharedRouter$1$1@80e75f5d)\\nPelletHTTPRoute(method=POST, uri=/v1/hello, handler=dev.pellet.demo.DemoKt$main$1$sharedRouter$1$2@80e75f5d)\",\"name\":\"dev.pellet.server.PelletServer\",\"thread\":\"main\"}\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.762581Z\",\"message\":\"Pellet started in 145ms\",\"name\":\"dev.pellet.server.PelletServer\",\"thread\":\"main\"}\n```\n\nThen you can hit this endpoint locally using [httpie](https://httpie.io/):\n```\n🥕 carrot 🗂 ~/git/pellet $ http -v localhost:8082/v1/hello\nGET /v1/hello HTTP/1.1\nAccept: */*\nAccept-Encoding: gzip, deflate\nConnection: keep-alive\nHost: localhost:8082\nUser-Agent: HTTPie/2.6.0\n\n\n\nHTTP/1.1 204 No Content\nX-Hello: World\n```\n\nWhich produces a request log line, including basic structured log elements like request duration and response code, like so:\n```\n{\"level\":\"info\",\"timestamp\":\"2022-02-27T22:23:38.762581Z\",\"message\":\"127.0.0.1 - - [05/Mar/2022:23:48:27 0000] \\\"GET /v1/hello HTTP/1.1\\\" 204 0\",\"name\":\"dev.pellet.server.codec.http.HTTPRequestHandler\",\"thread\":\"DefaultDispatcher-worker-3\",\"request.method\":\"GET\",\"request.uri\":\"/v1/hello\",\"response.code\":204,\"response.duration_ms\":1}\n```\n\nErrors thrown in handlers are logged appropriately:\n```\n{\"level\":\"error\",\"timestamp\":\"2022-03-06T00:03:58.526635Z\",\"message\":\"failed to handle request\",\"name\":\"dev.pellet.server.codec.http.HTTPRequestHandler\",\"thread\":\"DefaultDispatcher-worker-2\",\"throwable\":\"java.lang.RuntimeException: intentional error\\n\\tat dev.pellet.demo.DemoKt.handleForceError(Demo.kt:76)\\n\\tat dev.pellet.demo.DemoKt.access$handleForceError(Demo.kt:1)\\n\\tat dev.pellet.demo.DemoKt$main$1$sharedRouter$1$3.handle(Demo.kt:19)\\n\\tat dev.pellet.demo.DemoKt$main$1$sharedRouter$1$3.handle(Demo.kt:19)\\n\\tat dev.pellet.server.codec.http.HTTPRequestHandler.handle(HTTPRequestHandler.kt:62)\\n\\tat dev.pellet.server.codec.http.HTTPMessageCodec.consume(HTTPMessageCodec.kt:94)\\n\\tat dev.pellet.server.connector.SocketConnector.readLoop(SocketConnector.kt:76)\\n\\tat dev.pellet.server.connector.SocketConnector.access$readLoop(SocketConnector.kt:18)\\n\\tat dev.pellet.server.connector.SocketConnector$readLoop$1.invokeSuspend(SocketConnector.kt)\\n\\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)\\n\\tat kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)\\n\\tat kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)\\n\\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)\\n\\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)\\n\\tat kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)\\n\"}\n{\"level\":\"info\",\"timestamp\":\"2022-03-06T00:03:58.541244Z\",\"message\":\"127.0.0.1 - - [06/Mar/2022:00:03:58 0000] \\\"GET /v1/error HTTP/1.1\\\" 500 0\",\"name\":\"dev.pellet.server.codec.http.HTTPRequestHandler\",\"thread\":\"DefaultDispatcher-worker-2\",\"request.method\":\"GET\",\"request.uri\":\"/v1/error\",\"response.code\":500,\"response.duration_ms\":2}\n```\n\nPellet integrates nicely with `kotlinx.serialization`. For example, you can define a handler:\n```kotlin\n@kotlinx.serialization.Serializable\ndata class ResponseBody(\n    val message: String\n)\n\nprivate fun handleResponseBody(\n    context: PelletHTTPRouteContext\n): HTTPRouteResponse {\n    val responseBody = ResponseBody(message = \"hello, world 🌎\")\n    return HTTPRouteResponse.Builder()\n        .statusCode(200)\n        .jsonEntity(Json, responseBody)\n        .header(\"X-Hello\", \"World\")\n        .build()\n}\n```\n\nWhich will respond like so:\n```\n🥕 carrot 🗂 ~/git/pellet $ http localhost:8082/v1/hello\nHTTP/1.1 200 OK\nContent-Length: 31\nContent-Type: application/json\nX-Hello: World\n\n{\n    \"message\": \"hello, world 🌎\"\n}\n```\n\nPellet supports type-safe route building and variable matching:\n```kotlin\nval idDescriptor = uuidDescriptor(\"id\")\nval suffixDescriptor = stringDescriptor(\"suffix\")\nval helloIdPath = PelletHTTPRoutePath.Builder()\n    .addComponents(\"/v1\")\n    .addVariable(idDescriptor)\n    .addComponents(\"/hello\")\n    .build()\n@kotlinx.serialization.Serializable\ndata class ResponseBody(\n    val message: String\n)\nrouter {\n    get(helloIdPath) {\n        val id = it.pathParameter(idDescriptor).getOrThrow()\n        val suffix = it.firstQueryParameter(suffixDescriptor).getOrNull()\n            ?: \"👋\"\n        val responseBody = ResponseBody(message = \"hello $id $suffix\")\n        return HTTPRouteResponse.Builder()\n            .statusCode(200)\n            .jsonEntity(Json, responseBody)\n            .build()\n    }\n}\n```\n\nWhich will respond like so:\n```\n🥕 carrot 🗂 ~/git/pellet $ http localhost:8082/v1/06b39add-2b57-4d58-b084-40afeacab2e9/hello\nHTTP/1.1 200 OK\nContent-Length: 61\nContent-Type: application/json\n\n{\n    \"message\": \"hello 06b39add-2b57-4d58-b084-40afeacab2e9 👋\"\n}\n\n🥕 carrot 🗂 ~/git/pellet $ http localhost:8082/v1/06b39add-2b57-4d58-b084-40afeacab2e9/hello\\?suffix=🥕\nHTTP/1.1 200 OK\nContent-Length: 61\nContent-Type: application/json\n\n{\n    \"message\": \"hello 06b39add-2b57-4d58-b084-40afeacab2e9 🥕\"\n}\n```\n\nYou can group related paths, and type-safe path components, when building a router:\n```kotlin\nval idDescriptor = uuidDescriptor(\"id\")\nval router = httpRouter {\n    get(\"/\", ::handleRoot)\n    path(\"/v1\") {\n        get(\"/hello\", ::handleHello)\n        post(\"/echo\", ::handleEcho)\n        get(\"/error\", ::handleForceError)\n        path(idDescriptor) {\n            get(\"/hello\", ::handleNamedHello)\n        }\n    }\n}\n```\n\nWhich will produce the following route table:\n```\nGET / -\u003e ::handleRoot\nGET /v1/hello -\u003e ::handleHello\nPOST /v1/echo -\u003e ::handleEcho\nGET /v1/error -\u003e ::handleForceError\nGET /v1/{id:uuid}/hello -\u003e ::handleNamedHello\n```\n\nIt's easy to decode an incoming request body:\n```kotlin\n@kotlinx.serialization.Serializable\ndata class RequestBody(\n    val message: String\n)\n@kotlinx.serialization.Serializable\ndata class ResponseBody(\n    val message: String\n)\n\nprivate fun handleEchoRequest(\n    context: PelletHTTPRouteContext\n): HTTPRouteResponse {\n    val requestBody = context.decodeRequestBody\u003cRequestBody\u003e(Json).getOrElse {\n        return HTTPRouteResponse.Builder()\n            .badRequest()\n            .build()\n    }\n    val responseBody = ResponseBody(\n        message = requestBody.message\n    )\n    return HTTPRouteResponse.Builder()\n        .jsonEntity(Json, responseBody)\n        .build()\n}\n```\n\nWhich will echo the (well-formed) request like so:\n\n```\n🥕 carrot 🗂 ~/git/pellet $ http POST localhost:8082/v1/echo message=\"hello, world 🥕\"\nHTTP/1.1 200 OK\nContent-Length: 31\nContent-Type: application/json\n\n{\n    \"message\": \"hello, world 🥕\"\n}\n```\n\nYou can find more examples in the `demo` subproject.\n\n# License\n\nThis work is, unless otherwise stated, licensed under the Apache License, Version 2.0.\n\n```\nCopyright 2021-2024 lopcode\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flopcode%2Fpellet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flopcode%2Fpellet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flopcode%2Fpellet/lists"}