{"id":15928075,"url":"https://github.com/mrizzi/restop","last_synced_at":"2026-03-08T04:31:43.174Z","repository":{"id":105112624,"uuid":"276308900","full_name":"mrizzi/restop","owner":"mrizzi","description":"REST OPinionated framework","archived":false,"fork":false,"pushed_at":"2025-05-28T18:01:44.000Z","size":81,"stargazers_count":1,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-09T16:11:25.844Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrizzi.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,"zenodo":null}},"created_at":"2020-07-01T07:32:50.000Z","updated_at":"2021-08-09T22:11:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"e4488774-54bf-4d86-b757-df60692dbf1d","html_url":"https://github.com/mrizzi/restop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mrizzi/restop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrizzi%2Frestop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrizzi%2Frestop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrizzi%2Frestop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrizzi%2Frestop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrizzi","download_url":"https://codeload.github.com/mrizzi/restop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrizzi%2Frestop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30245220,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T00:58:18.660Z","status":"online","status_checked_at":"2026-03-08T02:00:06.215Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2024-10-06T23:21:23.580Z","updated_at":"2026-03-08T04:31:43.154Z","avatar_url":"https://github.com/mrizzi.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# restop\n\n## Introduction\n\nRestop is an REST OPinionated Quarkus extension.  \nIt's meant to avoid keep rewriting over and over (almost) the same code to create resource REST endpoints when wrinting Quarkus applications leveraging Hibernate with Panache.  \n\nThe opinions that drives the opinionated approach are:\n\n* a \"list all\" endpoint should be always paginated to provide a stable and predictable impact of each endpoint\n* a \"list all\" endpoint should let the user to be able to filter data \n* every endpoint should allow the usage of DTOs to not expose \"internal\" entities\n* it relies on \"active record pattern\" for Hibernate with Panache Quarkus extension\n* multiple inheritance of behavior leveraging Java interfaces default methods\n\nThese are opinions (and as such are debatable) so this is not supposed to be the solution for everything but a solution that works on quite a lot of use cases (more below).\n\n## Usage\n\n### Add Dependency\n\n[JitPack](https://jitpack.io) can be used to add restop dependency:\n\n1. add the JitPack repository\n\n    ```xml\n    \u003crepositories\u003e\n        \u003crepository\u003e\n            \u003cid\u003ejitpack.io\u003c/id\u003e\n            \u003curl\u003ehttps://jitpack.io\u003c/url\u003e\n        \u003c/repository\u003e\n    \u003c/repositories\u003e\n    ```\n\n1. add the dependency\n    ```xml\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.github.mrizzi\u003c/groupId\u003e\n            \u003cartifactId\u003erestop\u003c/artifactId\u003e\n            \u003cversion\u003emaster-SNAPSHOT\u003c/version\u003e\n        \u003c/dependency\u003e\n    ```\n\n### Read endpoints\n\nLet's start with the `Fruit` entity referenced in many Quarkus guides.  \n\nIt **must be a `PanacheEntity`** like:\n\n```java\n@Entity\npublic class Fruit extends PanacheEntity {\n\n    @Column(length = 40, unique = true)\n    public String name;\n    public String description;\n\n    public Fruit() {\n    }\n\n    public Fruit(String name, String description) {\n        this.name = name;\n        this.description = description;\n    }\n}\n```\n\nNow let's move to leverage Restop to create the \"read\" endpoints:\n\n```java\n@Path(\"fruit\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitResource implements ReadableById\u003cFruit\u003e, ReadablePaginatedByRange\u003cFruit\u003e {\n    @Override\n    public Class\u003cFruit\u003e getType() {return Fruit.class;}\n}\n```\n\ndone!  \nThe `/fruit` endpoint will be able to provide responses to calls:\n\n* *read one*: `GET` request to `/fruit/{id}` endpoint with a single `Fruit` result\n* *read many \"first page\"*: `GET` request to `/fruit` endpoint with an ordered (by ID) list of `Fruit` results using default (and opinionated) values for `limit` (i.e. `25`) and `offset` (i.e. `0`) parameters\n* *read many \"n-th page\"*:`GET` request to `/fruit?limit=10\u0026offset=20` endpoint with an ordered (by ID) list of (up to 10) `Fruit` results starting for the 20th element \n* *read many with sorting*:`GET` request to `/fruit?sort=name:Ascending` endpoint with an ordered by Fruit's `name` field ascending list of (up to 25) `Fruit` results starting for first element. Note that sorting works on multiple fields using the syntax `sort=field1:asc,field2:desc`.  \n`a`, `asc` and `ascending` can be used for ascending direction (it's the default and fallback direction) and `d`, `desc` and `descending` for descending direction  \n* *read many with \"equals\" filter*:`GET` request to `/fruit?name=Banana` endpoint with an ordered (by ID) list of (up to 25) `Fruit` results whose `name` field value is `Banana`\n* *read many with \"in\" filter*:`GET` request to `/fruit?name=Banana\u0026name=Apple\u0026name=Kiwi` endpoint with an ordered (by ID) list of (up to 25) `Fruit` results whose `name` field value is `Banana` or `Apple` or `Kiwi`\n\nObviously the \"query\" parameters for the \"read many\" operations work together so they can be combined in the request.\n\nHere is an example of a \"paginated\" response (for the `GET` request to `/fruit?sort=name:Ascending` endpoint):\n```json\n{\n    \"data\": [\n        {\n            \"id\": 2,\n            \"description\": \"Winter fruit\",\n            \"name\": \"Apple\"\n        },\n        {\n            \"id\": 3,\n            \"description\": \"Tropical fruit\",\n            \"name\": \"Banana\"\n        },\n        {\n            \"id\": 1,\n            \"description\": \"Sweet fruit available on mid-spring.\",\n            \"name\": \"Cherry\"\n        }\n    ],\n    \"links\": {\n        \"first\": \"/fruits?limit=25\u0026offset=0\u0026sort=name:Ascending\",\n        \"last\": \"/fruits?limit=25\u0026offset=0\u0026sort=name:Ascending\"\n    },\n    \"meta\": {\n        \"count\": 3,\n        \"limit\": 25,\n        \"offset\": 0,\n        \"sort\": \"name:Ascending\"\n    }\n}\n```\nwhere:\n\n* `data` contains the response data array\n* `links` set of links to easily move to other set of results consistently with the request's `limit` and `offset`\n  * `first` link to the first page\n  * `prev` link to the previous page (if available)\n  * `next` link to the next page (if available)\n  * `last` link to the last page\n* `meta` contains metadata about the resource\n  * `count` is the total number of entities corresponding to the filters\n  * `limit` is the limit applied to the data retrieved (useful in case of default values)\n  * `offset` is the offset applied to the data retrieved (useful in case of default values)\n  * `sort` is the sorting applied to the data retrieved (useful in case of default values)\n\nTo recap what has been done so far:\n\n1. created `Fruit extends PanacheEntity` entity (something that should have been done anyway)\n1. created `FruitResource implements ReadableById\u003cFruit\u003e, ReadablePaginatedByRange\u003cFruit\u003e` providing the `getType()` method implementation (requested from Restop)\n1. got for free all the \"read\" endpoints about with pagination, sorting, filtering and links\n\n### Create endpoint\n\nHow to add the endpoint to create a `Fruit` entity?  \nFollowing the \"multiple inheritance of behavior\" approach, it's a matter of adding to `FruitResource` class that it implements the `Creatable\u003cE extends PanacheEntity\u003e` interface.  \nThe class will look like:\n\n```java\n@Path(\"fruit\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitResource implements ReadableById\u003cFruit\u003e, ReadablePaginatedByRange\u003cFruit\u003e,\n                                      Creatable\u003cFruit\u003e {\n    @Override\n    public Class\u003cFruit\u003e getType() {return Fruit.class;}\n}\n```\n\nNow a `POST` request to `/fruit` endpoint will create a new `Fruit` resource.  \n \n### Delete endpoint\n\nFor adding the deletion feature to an endpoint the approach will be the same as above.  \nChange the `FruitResource` class to implement the `Deletable\u003cE extends PanacheEntity\u003e` interface.  \n\nWith this further change, the `FruitResource` class will be:\n\n```java\n@Path(\"fruit\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitResource implements ReadableById\u003cFruit\u003e, ReadablePaginatedByRange\u003cFruit\u003e,\n                                      Creatable\u003cFruit\u003e, Deletable\u003cFruit\u003e {\n    @Override\n    public Class\u003cFruit\u003e getType() {return Fruit.class;}\n}\n```\nNow a request `DELETE` request to the `/fruit/{id}` endpoint will delete the `Fruit` with provided `id`.\n\n### Update endpoint\n\nNo interface available for adding the update feature yet.  \nLet me clarify the update use case.  \nThe \"sample\" implementation of the update endpoint is something like:\n\n```java\n@PUT\n@Path(\"{id}\")\n@Transactional\npublic Fruit update(@PathParam Long id, Fruit fruit) {\n    if (fruit.name == null) {\n        throw new WebApplicationException(\"Fruit Name was not set on request.\", 422);\n    }\n\n    Fruit entity = Fruit.findById(id);\n\n    if (entity == null) {\n        throw new WebApplicationException(\"Fruit with id of \" + id + \" does not exist.\", 404);\n    }\n\n    entity.name = fruit.name;\n\n    return entity;\n}\n```\n\nas you can see, in this case there a need for a \"knowledge\" about the bean to move the information from the `fruit` input bean to the `entity` bean to get persisted into the database.  \nThe interfaces introduces so far are not taking into account any kind of knowledge about the bean and to keep this approach, there's no `Updatable` interface.  \n\nBut, no worries, this takes us to the next set of features: DTO.\n\n## Usage with DTO\n\nThe last paragraph about being able to reflect changes from an input bean into the persisted bean is close to the DTO (data transfer object) approach.  \nMore generally speaking, when working with REST endpoints is common to have the need to use DTO for the endpoints avoiding to use the entities bean in the REST APIs.\n\nrestop provides a way to create quickly and easily REST endpoints with DTO.  \nThe main change to be introduced to use DTO is the need to implement the `getMapper()` method from the `WithDtoWebMethod` interface.\nThe aim of this method is to provide the implementation of a mapper that takes case of \"translating\" values from DTO to Entity beans.\n  \nDTO, as everything in restop, are opinionated as well and they must (right now, maybe this will change in the future) accomplish one requirement:  \n**DTO's fields must be a subset of Entity's fields**.\n\nLet's see how it works starting from where we left,  the update method.\n\n###  Update with DTO endpoint\n\nTo add the endpoint for updating entities the `FruitResource` class must implement also the `UpdatableWithDto\u003cE extends PanacheEntity, D\u003e` interface.\nIn this example, the declaration will use `UpdatableWithDto\u003cFruit, Fruit\u003e` since our DTO corresponds with the entity: this is an edge case obviously but for the sake of the example it makes sense.  \n\nSo `FruitResource` becomes:\n\n```java\n@Path(\"fruit\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitResource implements ReadableById\u003cFruit\u003e, ReadablePaginatedByRange\u003cFruit\u003e,\n                                      Creatable\u003cFruit\u003e, Deletable\u003cFruit\u003e, UpdatableWithDto\u003cFruit, Fruit\u003e{\n    @Override\n    public Class\u003cFruit\u003e getType() {return Fruit.class;}\n\n    @Override\n    public Mapper\u003cFruit, Fruit\u003e getMapper() {\n        return new Mapper\u003cFruit, Fruit\u003e() {\n            @Override\n            public Fruit map(Fruit source, Fruit target) {\n                if (target == null) target = new Fruit();\n                target.name = source.name;\n                target.description = source.description;\n                return target;\n            }\n        };\n    }}\n```\n\nSo, besides adding the interface, also the implementation for the `getMapper()` method has been added.  \nThe implementation of the method is basic but it does what we expect: it copies values from DTO (a.k.a. `source`) into entity (a.k.a. `target`).  \n\nNow a `PUT` request to the `/fruit/{id}` endpoint will update the `Fruit` with provided `id` using the values in the DTO.  \n\nIt's also clear this is an edge case of having DTO because the entity and the DTO are the same (compliant with the \"subset\" requirement): in the next paragraph we will see how to use a \"traditional\" (and obviously opinionated) DTO.\n\n### Reads with DTO endpoint\n\nWhen dealing with read operations, it happens that some entity bean's fields are not meant to be sent out to the client.  This can happen for different reasons: some fields are just internal fields (e.g. audit fields) or maybe you want to create a response with just the field shown in the UI in order to maximize the perfomances reading from the DB only the needed fields and so keeping the response payload as small as possible.  \n \nGoing back to our example, let's say (and really just for the sake of the explanation) we just want to send out the `name` of the `Fruit` entities and not their `id` and `description`.\nThe DTO will look like:  \n\n```java\n@RegisterForReflection\npublic class FruitDto {\n\n    public String name;\n\n    public FruitDto(String name) {\n        this.name = name;\n    }\n}\n```\n\nThe annotation `@RegisterForReflection` is mandatory to register manually the projection class for reflection, if you plan to deploy your application as a native executable (ref. [Simplified Hibernate ORM with Panache](https://quarkus.io/guides/hibernate-orm-panache#query-projection))\n\nFor the example to use the \"DTO-ed\" operations, we can create a new resource class `FruitWithDtoResource` like this:\n\n```java\n@Path(\"fruits-dto\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitWithDtoResource implements ReadableByIdWithDto\u003cFruit, FruitDto\u003e, ReadablePaginatedByRangeWithDto\u003cFruit, FruitDto\u003e {\n\n    @Override\n    public Class\u003cFruit\u003e getPanacheEntityType() {return Fruit.class;}\n\n    @Override\n    public Class\u003cFruitDto\u003e getDtoType() {return FruitDto.class;}\n}\n```\n\nso, comparing quickly with the above `FruitResource` class:\n\n* `ReadableById\u003cFruit\u003e` has been replaced by `ReadableByIdWithDto\u003cFruit, FruitDto\u003e`\n* `ReadablePaginatedByRange\u003cFruit\u003e` has been replaced by `ReadablePaginatedByRangeWithDto\u003cFruit, FruitDto\u003e`\n* `getDtoType()` method has been added and implemented\n\nWith just this code, we have all the same read endpoint described in the above [Reads endpoint](#read-endpoints) paragraph.  \n\n### Create with DTO endpoint\n\nAs above, we can add a method to create an entity using a DTO implementing the `CreatableWithDto\u003cE extends PanacheEntity, D\u003e` interface and hence providing the requested implementation of the `getMapper()` method.\n\nThe `FruitWithDtoResource` will become:\n\n```java\n@Path(\"fruits-dto\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitWithDtoResource implements ReadableByIdWithDto\u003cFruit, FruitDto\u003e, ReadablePaginatedByRangeWithDto\u003cFruit, FruitDto\u003e,\n        CreatableWithDto\u003cFruit, FruitDto\u003e {\n\n    @Override\n    public Class\u003cFruit\u003e getPanacheEntityType() {return Fruit.class;}\n\n    @Override\n    public Class\u003cFruitDto\u003e getDtoType() {return FruitDto.class;}\n\n    @Override\n    public Mapper\u003cFruit, FruitDto\u003e getMapper() {\n        return new Mapper\u003cFruit, FruitDto\u003e() {\n            @Override\n            public Fruit map(FruitDto source, Fruit target) {\n                if (target == null) target = new Fruit();\n                target.name = source.name;\n                return target;\n            }\n        };\n    }\n}\n```\n\n`FruitDto` needs a change as well to annotate the only available constructor (with one input parameter) with `@JsonbCreator` annotation since otherwise during deserialization the no-arg constructor is searched and since it's not available, the create endpoints will fail.  \nIf you're wondering why not just adding the no-arg constructor, the reason is that to use the same bean in the read operations as a projection, the bean must have just one single constructor will all the fields to have Hibernate to create the right select statement for the \"projected\" query (ref. [Simplified Hibernate ORM with Panache](https://quarkus.io/guides/hibernate-orm-panache#query-projection)).  \nSo `FruitDto` is:\n\n```java\n@RegisterForReflection\npublic class FruitDto {\n\n    public String name;\n\n    @JsonbCreator\n    public FruitDto(String name) {\n        this.name = name;\n    }\n}\n```\n\nLet's say that in this case using the `FruitDto` doesn't make a lot of sense because it will add `Fruits` without description but this example is provided just to show how to use DTO and then it's left to the user when it makes sense to use `Creatable` or `CreatableWithDto` interfaces.\n\n### Delete with DTO endpoint\n\nThere's nothing about this since there's no DTO involved in deleting an entity: make a `DELETE` request to the `/fruit/{id}` endpoint will delete the `Fruit` with provided `id`.\nSo use `Deletable\u003cE extends PanacheEntity\u003e` interface and let the `FruitWithDtoResource` become:\n\n```java\n@Path(\"fruits-dto\")\n@ApplicationScoped\n@Produces(MediaType.APPLICATION_JSON)\n@Consumes(MediaType.APPLICATION_JSON)\npublic class FruitWithDtoResource implements ReadableByIdWithDto\u003cFruit, FruitDto\u003e, ReadablePaginatedByRangeWithDto\u003cFruit, FruitDto\u003e,\n        CreatableWithDto\u003cFruit, FruitDto\u003e, Deletable\u003cFruit\u003e {\n\n    @Override\n    public Class\u003cFruit\u003e getPanacheEntityType() {return Fruit.class;}\n\n    @Override\n    public Class\u003cFruitDto\u003e getDtoType() {return FruitDto.class;}\n\n    @Override\n    public Mapper\u003cFruit, FruitDto\u003e getMapper() {\n        return new Mapper\u003cFruit, FruitDto\u003e() {\n            @Override\n            public Fruit map(FruitDto source, Fruit target) {\n                if (target == null) target = new Fruit();\n                target.name = source.name;\n                return target;\n            }\n        };\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrizzi%2Frestop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrizzi%2Frestop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrizzi%2Frestop/lists"}