{"id":13879464,"url":"https://github.com/sammyhenningsson/shaf","last_synced_at":"2025-09-23T13:12:37.168Z","repository":{"id":28781548,"uuid":"119308296","full_name":"sammyhenningsson/shaf","owner":"sammyhenningsson","description":"A framework for creating hypermedia driven APIs in Sinatra","archived":false,"fork":false,"pushed_at":"2024-03-01T17:04:56.000Z","size":1222,"stargazers_count":18,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-02-28T17:57:27.728Z","etag":null,"topics":["framework","hal-json","hypermedia-api","rest","rest-api","ruby","sequel","sinatra"],"latest_commit_sha":null,"homepage":"","language":"Ruby","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/sammyhenningsson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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}},"created_at":"2018-01-28T23:38:30.000Z","updated_at":"2024-12-31T01:53:43.000Z","dependencies_parsed_at":"2024-01-13T20:57:16.199Z","dependency_job_id":"b0c99084-5353-442f-87cc-7cc260c8f6a8","html_url":"https://github.com/sammyhenningsson/shaf","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fshaf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fshaf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fshaf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sammyhenningsson%2Fshaf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sammyhenningsson","download_url":"https://codeload.github.com/sammyhenningsson/shaf/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243982182,"owners_count":20378605,"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":["framework","hal-json","hypermedia-api","rest","rest-api","ruby","sequel","sinatra"],"created_at":"2024-08-06T08:02:21.904Z","updated_at":"2025-09-23T13:12:32.115Z","avatar_url":"https://github.com/sammyhenningsson.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Shaf (Sinatra Hypermedia API Framework)\n[![Gem Version](https://badge.fury.io/rb/shaf.svg)](https://badge.fury.io/rb/shaf)\n![CI](https://github.com/sammyhenningsson/shaf/workflows/CI/badge.svg)  \nShaf is a framework for building hypermedia driven REST APIs. Its goal is to be like a lightweight version of `rails new --api` with hypermedia as a first class citizen. Instead of reinventing the wheel Shaf uses [Sinatra](http://sinatrarb.com/) and adds a layer of conventions similar to [Rails](http://rubyonrails.org/). It uses [Sequel](http://sequel.jeremyevans.net/) as ORM and [HALPresenter](https://github.com/sammyhenningsson/hal_presenter) for policies and serialization (which means that the main mediatype is [HAL](http://stateless.co/hal_specification.html)).  \nMost APIs claiming to be RESTful completly lacks the concept of links and relies upon clients to construction urls to _known_ endpoints. Thoses APIs are missing some of the concepts that Roy Fielding put together in is dissertation about REST.  \nIf you don't have full understanding of what REST is then that's fine. Though you are encouraged to read up on the basics. Check out [this blog](https://apisyouwonthate.com/blog/rest-and-hypermedia-in-2019) for a great explanation of the building blocks of REST.  \nA short version is: REST was \"invented\" by describing how the web is architectured. Web components (e.g browsers, servers, cache proxies etc) all use the same interface, where URIs and mediatypes play a big part. This enables any browser to connect to any web server without prior knowledge about each other. An important part of this is to use hypermedia links, which makes it possible for components to evolve independently.\n\nBuilding a REST API requires knowledge about standards and a lot of boring stuff. Shaf aims to reduce those prerequirements, minimize bikeshedding and to get you up and running quickly. Some of the benefits of using Shaf is that you get:\n - Scaffolding\n - Serialization\n - Authorization\n - Content negotiation\n - Documentation\n - Forms\n - Uri helpers\n - Pagination\n - Testing\n - HTTP caching\n - Link preloading (enables HTTP2 Push)\n\n## What's unique about Shaf?\nThe list above could be implemented in any web framework. So why use Shaf? Well, if you are comfortable writing it yourself, then perhaps Shaf might not be for you. However it can still be nice to have some conventions to rely upon, instead of having to decide all basic details. Like choosing a mediatype for instance (this is something people have very strong/different opinions about).  \nI don't think there's another Ruby web framework that emphasizes the principles of REST as much as Shaf does.\nAn example of a unique feature (AFAIK) is that mediatypes are separated from controller actions. In most other frameworks, each controller action specifies the possible content type to be returned.\nIn Shaf, controller actions returns Ruby objects. Depending on what clients want to receive (specified by the `Accept` header),\nthe appropriate serializer is looked up and used to respond with the right representation. (This is not 100% true, since there's actually a helper, `respond_with`, that produces the usual `[status_code, headers, response]`. However you would still see it as an object being returned and serialization takes place afterwards. Parsing inputs is done using a similar approach.  \nThe most unique feature is probably the usage of mediatype profiles, which are used both for machine readable definitions and for generating documentation.(See [Mediatype profiles](doc/PROFILES.md) for more information.)\n\n## Getting started\nInstall Shaf with\n```sh\ngem install shaf\n```\nThen create a new project with `shaf new` followed by the name of the project. E.g.\n```sh\nshaf new blog\n```\nThis will create a new directory with a bunch of files that make up the basics of a new API. Change into the this directory and install any missing depencencies.\n```sh\ncd blog\nbundle\n```\nYour newly created project should contain the following files:\n```sh\n.\n├── api\n│   ├── controllers\n│   │   ├── base_controller.rb\n│   │   ├── docs_controller.rb\n│   │   └── root_controller.rb\n│   ├── policies\n│   │   └── base_policy.rb\n│   └── serializers\n│       ├── base_serializer.rb\n│       ├── documentation_serializer.rb\n│       ├── error_serializer.rb\n│       ├── form_serializer.rb\n│       ├── root_serializer.rb\n│       └── validation_error_serializer.rb\n├── config\n│   ├── bootstrap.rb\n│   ├── customize.rb\n│   ├── database.rb\n│   ├── database.yml\n│   ├── directories.rb\n│   ├── helpers.rb\n│   ├── initializers\n│   │   ├── authentication.rb\n│   │   ├── db_migrations.rb\n│   │   ├── hal_presenter.rb\n│   │   ├── logging.rb\n│   │   └── sequel.rb\n│   ├── initializers.rb\n│   ├── paths.rb\n│   └── settings.yml\n├── config.ru\n├── frontend\n│   ├── assets\n│   │   └── css\n│   │       └── main.css\n│   └── views\n│       ├── form.erb\n│       ├── headers.erb\n│       ├── layout.erb\n│       └── payload.erb\n├── Gemfile\n├── Gemfile.lock\n├── Rakefile\n└── spec\n    ├── integration\n    │   └── root_spec.rb\n    ├── serializers\n    │   └── root_serializer_spec.rb\n    └── spec_helper.rb\n```\nYou now have a functional API. Start the server with\n```sh\nshaf server\n```\nThen in another terminal run\n```sh\ncurl localhost:3000/\n```\nWhich should return the following payload.\n```sh\n{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"http://localhost:3000/\"\n    }\n  }\n}\n```\n\n_Hint_: The output will actually not have any newlines and will look a bit more dense. To make the output more readable pipe the\ncurl command to `jq` (which is a great a tool for dealing with json strings).\n```sh\ncurl localhost:3000/ | jq\n```\nOr if you don't have `jq` installed, you can also pretty print json through Ruby. E.g:\n```sh\ncurl localhost:3000/ | ruby -rjson -e \"puts JSON.pretty_generate(JSON.parse(STDIN.read))\"\n```\n\nThe project also contains a few specs that you can run with `rake`\n```sh\nshaf test\n```\n\nCurrently your API is pretty useless. Let's fix that by generating some scaffolding. The following command will create a new resource with two attributes (`title` and `message`).\n```sh\nshaf generate scaffold post title:string message:string \n```\nThis will output:\n```sh\nAdded:      api/models/post.rb\nAdded:      db/migrations/20180224225335_create_posts_table.rb\nAdded:      api/serializers/post_serializer.rb\nAdded:      spec/serializers/post_serializer_spec.rb\nAdded:      api/policies/post_policy.rb\nAdded:      api/profiles/post.rb\nAdded:      api/forms/post_forms.rb\nAdded:      api/controllers/posts_controller.rb\nAdded:      spec/integration/posts_controller_spec.rb\nModified:   api/serializers/root_serializer.rb\n```\nAs shown in the output, that command created, a model, a controller, a serializer and a policy. It also generated a DB migration file, some forms, some specs and a link to the new `post` collection was added the root resource. So let's check this out by migrating the DB and restarting the server. Close any running instance with `Ctrl + C` and then:\n```sh\nrake db:migrate\nshaf server\n```\nAgain in another terminal run\n```sh\ncurl localhost:3000/ | jq\n```\nWhich should now return the following payload.\n```sh\n{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"http://localhost:3000/\"\n    },\n    \"posts\": {\n      \"href\": \"http://localhost:3000/posts\"\n    }\n  }\n}\n```\nThe root payload should now contain a link with rel _posts_. Lets follow that link..\n```sh\ncurl localhost:3000/posts | jq\n```\nThe response looks like this\n```sh\n{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"http://localhost:3000/posts?page=1\u0026per_page=25\"\n    },\n    \"up\": {\n      \"href\": \"http://localhost:3000/\"\n    },\n    \"create-form\": {\n      \"href\": \"http://localhost:3000/post/form\"\n    },\n    \"curies\": [\n      {\n        \"name\": \"doc\",\n        \"href\": \"http://localhost:3000/doc/profiles/post{#rel}\",\n        \"templated\": true\n      }\n    ]\n  },\n  \"_embedded\": {\n    \"posts\": []\n  }\n}\n```\nThis is the collection of posts (which currently is empty, see `$response['_embedded']['posts']`). Notice the link with rel _create-form_. This is the api telling us that we may add new post resources. Let's follow that link!\n```sh\ncurl http://localhost:3000/post/form | jq\n```\nThe response looks like this\n```sh\n{\n  \"method\": \"POST\",\n  \"name\": \"create-post\",\n  \"title\": \"Create Post\",\n  \"href\": \"http://localhost:3000/posts\",\n  \"type\": \"application/json\",\n  \"submit\": \"save\",\n  \"_links\": {\n    \"profile\": {\n      \"href\": \"http://localhost:3000/doc/profiles/shaf-form\"\n    },\n    \"self\": {\n      \"href\": \"http://localhost:3000/post/form\"\n    },\n    \"curies\": [\n      {\n        \"name\": \"doc\",\n        \"href\": \"http://localhost:3000/doc/profiles/shaf-form{#rel}\",\n        \"templated\": true\n      }\n    ]\n  },\n  \"fields\": [\n    {\n      \"name\": \"title\",\n      \"type\": \"string\"\n    },\n    {\n      \"name\": \"message\",\n      \"type\": \"string\"\n    }\n  ]\n}\n```\nThis form shows us how to create new post resources (see [Forms](doc/FORMS.md) for more info). A new post resource can be created with the following request \n```sh\ncurl -H \"Content-Type: application/json\" \\\n     -d '{\"title\": \"hello\", \"message\": \"lorem ipsum\"}' \\\n     localhost:3000/posts | jq\n```\nThe response shows us the new resource, with the attributes that we set as well as links for updating and deleting it.\n```sh\n{\n  \"title\": \"hello\",\n  \"message\": \"lorem ipsum\",\n  \"_links\": {\n    \"profile\": {\n      \"href\": \"http://localhost:3000/doc/profiles/post\"\n    },\n    \"collection\": {\n      \"href\": \"http://localhost:3000/posts\"\n    },\n    \"self\": {\n      \"href\": \"http://localhost:3000/posts/1\"\n    },\n    \"edit-form\": {\n      \"href\": \"http://localhost:3000/posts/1/edit\"\n    },\n    \"doc:delete\": {\n      \"href\": \"http://localhost:3000/posts/1\"\n    },\n    \"curies\": [\n      {\n        \"name\": \"doc\",\n        \"href\": \"http://localhost:3000/doc/profiles/post{#rel}\",\n        \"templated\": true\n      }\n    ]\n  }\n}\n```\nThis new resource is of course added to the collection of posts, which can now be retrieved by the link with rel _collection_.\n```sh\ncurl localhost:3000/posts | jq\n```\nResponse:\n```sh\n{\n  \"_links\": {\n    \"self\": {\n      \"href\": \"http://localhost:3000/posts?page=1\u0026per_page=25\"\n    },\n    \"up\": {\n      \"href\": \"http://localhost:3000/\"\n    },\n    \"create-form\": {\n      \"href\": \"http://localhost:3000/post/form\"\n    },\n    \"curies\": [\n      {\n        \"name\": \"doc\",\n        \"href\": \"http://localhost:3000/doc/profiles/post{#rel}\",\n        \"templated\": true\n      }\n    ]\n  },\n  \"_embedded\": {\n    \"posts\": [\n      {\n        \"title\": \"hello\",\n        \"message\": \"lorem ipsum\",\n        \"_links\": {\n          \"profile\": {\n            \"href\": \"http://localhost:3000/doc/profiles/post\"\n          },\n          \"collection\": {\n            \"href\": \"http://localhost:3000/posts\"\n          },\n          \"self\": {\n            \"href\": \"http://localhost:3000/posts/1\"\n          },\n          \"edit-form\": {\n            \"href\": \"http://localhost:3000/posts/1/edit\"\n          },\n          \"doc:delete\": {\n            \"href\": \"http://localhost:3000/posts/1\"\n          }\n        }\n      }\n    ]\n  }\n}\n```\n\n#### Recap\nWe have built a very basic hypermedia driven API with only one type of resource. The neatest thing about this is that it only took four commands:\n```sh\nshaf new blog\nbundle\nshaf generate scaffold post title:string message:string \nrake db:migrate\n```\n\n## Documentation\n### [Sinatra](doc/SINATRA.md)\n### [HAL mediatype](doc/HAL.md)\n### [Mediatype profiles](doc/PROFILES.md)\n### [Generators](doc/GENERATORS.md)\n### [Routing/Controllers](doc/ROUTING.md)\n### [Models](doc/MODELS.md)\n### [Forms](doc/FORMS.md)\n### [Serializers](doc/SERIALIZERS.md)\n### [Policies](doc/POLICIES.md)\n### [Authentication](doc/AUTHENTICATION.md)\n### [Settings](doc/SETTINGS.md)\n### [Database](doc/DATABASE.md)\n### [Mediatype profiles](doc/PROFILES.md)\n### [API Documentation](doc/DOCUMENTATION.md)\n### [HTTP Caching](doc/HTTP_CACHING.md)\n### [Pagination](doc/PAGINATION.md)\n### [Upgrading a shaf project](doc/UPGRADE.md)\n### [Testing](doc/TESTING.md)\n### [ShafClient](doc/SHAF_CLIENT.md)\n### [Rails](doc/RAILS.md)\n### [Frontend](doc/FRONTEND.md)\n### [Customizations](doc/CUSTOMIZATIONS.md)\n### [Business logic](doc/BUSINESS_LOGIC.md)\n\n\n## Contributing\nIf you find a bug or have suggestions for improvements, please create a new issue on Github. Pull request are welcome!\n\n## License\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsammyhenningsson%2Fshaf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsammyhenningsson%2Fshaf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsammyhenningsson%2Fshaf/lists"}