{"id":26415607,"url":"https://github.com/vertrical/tino","last_synced_at":"2025-03-18T00:53:54.773Z","repository":{"id":62421531,"uuid":"267833779","full_name":"Vertrical/tino","owner":"Vertrical","description":"Tiny and functional HTTP server for Deno with local JSON REST API for rapid prototyping.","archived":false,"fork":false,"pushed_at":"2022-01-20T10:40:46.000Z","size":233,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"develop","last_synced_at":"2025-03-17T13:40:59.867Z","etag":null,"topics":["async","chain","compose","deno","functional","http-server","javascript","json","jsondb","middlewares"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Vertrical.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-29T10:46:19.000Z","updated_at":"2023-09-19T17:38:00.000Z","dependencies_parsed_at":"2022-11-01T17:32:30.346Z","dependency_job_id":null,"html_url":"https://github.com/Vertrical/tino","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vertrical%2Ftino","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vertrical%2Ftino/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vertrical%2Ftino/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Vertrical%2Ftino/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Vertrical","download_url":"https://codeload.github.com/Vertrical/tino/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244135905,"owners_count":20403797,"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":["async","chain","compose","deno","functional","http-server","javascript","json","jsondb","middlewares"],"created_at":"2025-03-18T00:53:53.905Z","updated_at":"2025-03-18T00:53:54.764Z","avatar_url":"https://github.com/Vertrical.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### UPDATE: We stopped maintaining this project. If you have any requests about this project please create an Issue.\n\n\u003cimg src=\"https://tinoserver.s3-eu-west-1.amazonaws.com/tino-transparent.png\" width=\"250\" alt=\"Tino Logo\" /\u003e\n\n# Tino\n\nTiny HTTP server for Deno, functionally composed.\n\n## Install and Use\n\n1. Install Deno: https://deno.land/#installation\n2. Try it out: `$ deno run --allow-net --allow-read --allow-write https://deno.land/x/tino@v1.0.5/tino.js`\n\nInternally Tino uses `jsondb responder` which opens `/api` path for playing around. It uses `db.json` file by default as a database.\n\n1. To see it already, copy it from tests: `$ cp ./tests/jsondb.test.json ./db.json`\n2. Open http://localhost:8000/api\n\n## Minimal configuration (custom endpoints)\n\n```js\n// app.js\nimport tino from \"https://deno.land/x/tino@v1.0.5/tino.js\";\nconst app = tino.create();\nconst controller = () =\u003e ({ resp: \"pong\", status: 200 }); // must return { resp, status?, type? }\napp.get(() =\u003e ({ path: \"/ping\", use: controller }));\ntino.listen({ app, port: 8000 });\nconsole.log(\"Server running at 8000\");\n```\n\n1. Now run the server: `$ deno run --allow-net app.js`\n2. Send a request: `$ http :8000/ping` (HTTPie, curl, Postman, etc.)\n3. Receive `\"pong\"` as `text/plain` content type\n\n### Simple way of setting responses and statuses:\n\nSince all functions are composed and pure, it's easy to unit test them:\n\n```js\napp.get(() =\u003e ({\n  path: \"/notes/:id\",\n  use: ({ params, notes = { \"123\": { text: \"Take a walk\" } } }) =\u003e \n  (notes[params.id] ? { resp: notes[params.id] } : { status: 404, resp: \"Sorry, kinda nothing\" })\n}));\n```\nResulting in:\n```zsh\n$ http :8000/notes/123\n----------------------\nHTTP/1.1 200 OK\ncontent-length: 22\ncontent-type: application/json\n\n{\n  \"text\": \"Take a walk\"\n}\n```\n\n```zsh\n$ http :8000/notes/123456789\n----------------------------\nHTTP/1.1 404 Not Found\ncontent-length: 20\ncontent-type: text/plain\n\nSorry, kinda nothing\n```\n\n### Further configurations\n```js\napp.get(() =\u003e ({ path: \"/ping\", resp: \"pong\" })); // Shorter: Use `resp` directly with status 200\napp.get(() =\u003e ({ path: \"/ping-async\", use: async () =\u003e ({ resp: \"pong\", status: 201 }) })); // `use` controller can also be async\napp.not_found(() =\u003e ({ resp: \"Oops\" }));\n```\n\nTino application `app` supports following HTTP methods: GET, POST, PUT, PATCH, DELETE. Method names are lowercased. Also there is `not_found` for status 404, so you can define custom response.\n\n`resp` can be anything, but if it's a function it will return it's result of execution, and it will be called no matter if it's async or not. If it's an object (or returned as an object), content type will be `application/json`.\n\nThe only requirement for controller `use` is that it must return `{ resp, status?, type? }` object. It can also be defined as async function.\n\n### Difference between Tino and \"usual approach\"\n\nWhat I've seen and used is that usually there are one or more global variables or internally modified variables through the request cycle. This results in following pseudo code:\n\n```js\nmyController = ctx =\u003e {\n  ctx.type = \"text/html\";\n  ctx.status = 200;\n  ctx.body = \"\u003cp\u003eGreetings!\u003c/p\u003e\";\n}\n```\n\nWhile in Tino idea is that all functions are composed and there is no global variable but what you want in next step is what you pass further, through the chain. That might look like:\n\n```js\nmyController = () =\u003e {\n  const type = \"text/html\";\n  const status = 200;\n  const body = \"\u003cp\u003eGreetings!\u003c/p\u003e\";\n  return { type, status, body };\n}\n```\n\nIf you read further on about middlewares for example you'll see how this plays well with composition. These functions must be pure, easy to unit test and able to be recursive.\n\n## Defining path parameters\n\nParameters are defined as `:param` in your path definition. Optionals are defined as `:param?`. For example:\n```js\napp.get(() =\u003e ({ path: \"/user/:id\", use: ({ params }) =\u003e ({ resp: params.id }));\n```\n\n## `props` definition\n\nController `use` receives following parameters:\n\n1. `body` - body payload for POST, PUT or PATCH methods\n2. `params` - parameters from path definition, e.g. `/path/:id`\n3. `query` - query object from string like `?p=1\u0026q=2`\n4. `custom params` - anything else provided to method definition, except `path`, `resp` or `use`\n5. `matchedPath` - information about path regex\n6. `pathPattern` - information about path definition\n7. `req` - in form of `{ method, url }`\n7. Any other parameters coming from middlewares\n\nBasically you can test this with following have-it-all definition: (read more about middlewares below)\n```js\n// $ http POST :8000/post/123?q=1 foo=bar\nconst composed = withMiddlewares(\n  () =\u003e ({ isAdmin: true }),\n);\napp.post(() =\u003e ({\n  path: \"/post/:id\", // or optional with :id?\n  use: composed((props) =\u003e ({ resp: { ...props } })),\n  something: \"else\",\n}));\n```\n\n### Response\nResponse received should be:\n```json\n{\n  \"body\": {\n    \"foo\": \"bar\"\n  },\n  \"isAdmin\": true,\n  \"matchedPath\": {\n    \"index\": 0,\n    \"params\": {\n      \"id\": \"123\"\n    },\n    \"path\": \"/post/123\"\n  },\n  \"params\": {\n    \"id\": \"123\"\n  },\n  \"pathPattern\": \"/post/:id\",\n  \"query\": {\n    \"q\": \"1\"\n  },\n  \"req\": {\n    \"method\": \"POST\",\n    \"url\": \"/post2/123?q=1\"\n  },\n  \"something\": \"else\"\n}\n```\n\n### Return type (Content type)\n\nWhen you define `resp`, you can define content type such as `text/html`:\n```js\nconst use = () =\u003e ({ resp: \"\u003cp\u003eWorks!\u003c/p\u003e\", status: 200, type: 'text/html' });\napp.get(() =\u003e ({ path: \"/ping\", use  }));\n```\n\n## Middlewares\n\nMiddlewares offer you way to extend response by injecting additional information to your controllers. In Tino it is done by async functional composition so your middlewares can be both sync and async. It is offered by `withMiddlewares` helper from `tino.js`.\n\n### Examples:\n\n1. Check if user is admin and inject database into your controller:\n```js\nimport { withMiddlewares } from \"tino.js\";\n// Initial props are provided: https://github.com/Vertrical/tino#props-definition\nconst auth = (props) =\u003e ({ isUser: true });\nconst isAdmin = (props) =\u003e ({ isAdmin: false, ...props });\nconst withDB = (props) =\u003e ({ coll: {}, ...props });\nconst composed = withMiddlewares(auth, isAdmin, withDB);\n// Define your endpoint:\nconst use = composed(({ isUser, isAdmin, coll }) =\u003e ({ resp: \"Hello\", status: /*...*/ }));\napp.get(() =\u003e ({ path: \"/ping\", use }));\n```\nAny prop that is returned will be passed over to next function in chain, until the end - end result is what is passed to your controller.\n\n2. Exit early depending on if a precondition hasn't been met (protect the router):\n\nIt's similar to previous case only that if any of the middlewares throws an exception it will be used as end result of your controller, i.e. replace it.\n\n```js\nimport { withMiddlewares } from \"tino.js\";\nconst auth = (props) =\u003e { throw { resp: \"Boom\", status: 401 }; };\nconst isAdmin = (props) =\u003e ({ isAdmin: false, ...props });\nconst withDB = (props) =\u003e ({ coll: {}, ...props });\nconst composed = withMiddlewares(auth, isAdmin, withDB);\n// Define your endpoint:\nconst use = composed(({ isUser, isAdmin, coll }) =\u003e ({ resp: \"Hello\" }));\napp.get(() =\u003e ({ path: \"/ping\", use }));\n// HTTP Response headers and content: (if you call \"localhost:{port}/ping)\n`\nHTTP/1.1 401 Unauthorized\ncontent-length: 4\ncontent-type: text/plain\n\nBoom\n`\n```\nNote: Whatever you want to be returned from middlewares to your controller, you should propagate these props through the chain. (As seen above with `...props` for example)\n\n## Responders\n\nIn Tino responders are your implementations of custom APIs which don't rely on paths pattern matching.\n\nYou define a responder by adding `root: true` to your endpoint definition.\n\nFor example:\n```js\nimport myAwesomeAPI, { v2 } from \"somewhere\";\napp.any(() =\u003e ({ path: \"/awesome-api\", use: myAwesomeAPI, root: true })); // \u003c-- Notice the `root: true` part\napp.any(() =\u003e ({ path: \"/awesome-api/v2\", use: v2, root: true }));\n```\nSetting the `root` part is because we will match here only by `startsWith` against your request, disregarding any parameter matching.\n\nExample of a responder is `jsondb` which comes integrated with Tino and is located in [jsondb.js](https://github.com/Vertrical/tino/blob/develop/jsondb.js) file.\n\n### Using `jsondb` responder\n\nThis responder is just a small package included by default in Tino which handles CRUD operations on `db.json` file.\n\nEach response is wrapped with `response` parent, like:\n```js\n// GET /api/users/1234\n{\n  \"response\": {\n    \"id\": \"1234\",\n    \"name\": \"Smith\"\n  }\n}\n```\n\n### How is `jsondb` responder defined?\n\n`jsondb` responder is already integrated with Tino so you don't need to do anything. But, if you want to define it nevertheless, you can do it like below:\n\n```js\nimport tino, { jsondb } from \"tino.js\";\nconst app = tino.create();\napp.any(() =\u003e ({ path: \"/api\", use: jsondb(), root: true })); // notice the ()\n// If you want some other namespace\napp.any(() =\u003e ({ path: \"/awesome-api\", use: jsondb(), root: true }));\ntino.listen({ app });\n```\n(Please note that `jsondb` must be called. This is because it is a higher order function.)\n\n`any` is needed because we want ANY HTTP METHOD to be used with this.\n\n### JSON REST API\nTest JSON file is included in [tests/jsondb.test.json](https://github.com/Vertrical/tino/blob/develop/tests/jsondb.test.json). You need to create your `./db.json` file to operate agains it.\n\nHaving the content same as in jsondb.test.json file, we would have following requests returning respective responses:\n```sh\n# Get list of items:\n$ http :8000/api/laptops\n\n# Get item by id: (jsondb treats any \"id\" found in an entity as ID)\n$ http :8000/api/laptops/123\n\n# Create new item:\n$ http POST :8000/api/laptops id=789 brand=apple\n\n# Replace an item:\n$ http PUT :8000/api/laptops/789 brand=asus\n\n# Update an item:\n$ http PATCH :8000/api/laptops/789 brand=asus\n\n# DELETE an item:\n$ http DELETE :8000/api/laptops/789\n```\nYou can see many examples in [tests/requests.http](https://github.com/Vertrical/tino/blob/develop/tests/requests.http) file.\n\n### Customize API\n\nIf you want to change endpoint from `/api` to something else, just replace it:\n```js\napp.any(() =\u003e ({ path: \"/myapi\", use: jsondb(), root: true }));\n\n// you can optionally \"close\" /api\napp.any(() =\u003e ({ path: \"/api\", status: 404 }));\n```\nRemember that you need to create file `db.json` yourself. If not, the response will be empty and status 404.\n\n## CLI and options for Tino and jsondb\n\n### Dry run for jsondb\n\nIf you want only to check how a request would modify db.json database without touching it, you can do a dry run.\n```sh\n# --allow-write is not necessary\ndeno run --allow-net --allow-read tino.js --dry=true\n```\n\nIn your code you can achieve same by passing `true` to `jsondb` responder:\n```js\napp.get(() =\u003e ({ path: \"/ping\", use: jsondb(true), root: true }));\n```\n\n### Custom port\n\nYou can run Tino on custom port:\n```sh\ndeno run --allow-net --allow-read --allow-write tino.js --port=7000\n```\n\nSimilarly, in your code you can pass it to `listen` method:\n```js\n// if you omit port, it will be 8000\ntino.listen({ app, port: 7777 });\n```\n\n## Run tests\n\nRun: `$ deno test`\n\nAll tests are included in `./tests` directory.\n\n## Examples\n\nYou can find maintained list of exampes in [examples.js](https://github.com/Vertrical/tino/blob/develop/examples.js) file.\n\n## To-do list\n\n- [ ] Write TypeScript support (depends on https://github.com/microsoft/TypeScript/issues/38510)\n- [ ] The \"after hooks\", similar to middlewares but happening AFTER your controller\n- [ ] Cookies support\n- [ ] GraphQL responder for local prototyping (like jsondb for REST)\n\n## Stay in touch\n\nYou can follow us on Twitter https://twitter.com/tino_server or open issues here.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvertrical%2Ftino","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvertrical%2Ftino","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvertrical%2Ftino/lists"}