Little bit of theory
Since Deno released its version 1.0 earlier this year I was eager to make an HTTP server for it. And, few weeks later I had some basic routing... thing, I called Tino. It's tiny, it's for Deno.
Tino also offers local JSON REST API for rapid prototyping via responders. More on that when you scroll down.
Keep in mind, it is only JavaScript for now, so no TypeScript, but, that doesn't matter really since we can run both in Deno. We're waiting for this issue to become ready and TS 4.0 version bundled into Deno to make Tino fully TS, too. Why? Because variadic tuples are not yet supported and Tino uses function composition heavily.
I didn't want to have it "java-esque" behaviour with decorated routes and classes and what not (think of Spring Boot for example) only because it is now possible to write TypeScript out of the box. Not saying it's a bad thing, on the contrary.
On the other hand Tino uses only functions and (async) function composition. Async composition (for middlewares) is necessary so you can compose both sync and async functions.
Usage and examples
Let's see how it actually works and how much freedom one might have using functions only. (Take a look at examples any time)
First steps
Import Tino from third party modules (for version number look at README.md):
import tino from "https://deno.land/x/tino@vX.Y.Z/tino.js";
const app = tino.create();
// ... you business logic
tino.listen({ app, port: 8000 });
console.log(`Server running at 8000`);
Now we can focus on the rest of your logic, i.e. defining endpoints, controllers and responders.
Defining your first endpoint is as easy as:
const use = () => ({ resp: "pong" });
app.get(() => ({ path: "/ping", use }));
// Even shorter, but only for prototyping:
app.get(() => ({ path: "/ping", resp: "pong" }));
use
is your controller. It is flexible and used also for extending Tino's functionality and custom attributes. Read on for more on that.
Both use
and resp
can be functions, but it makes more sense for use
- if resp
is a function, it can still receive props but will be called internally and its return will be used as return for use
. ;)
Let's see what controller(use
) can be and what it can return:
// A function or async function, only returning a string (can be any primitive)
// content-type: text/plain
const returnPong = ({ resp: "pong" })
const use1 = () => returnPong;
const use2 = async () => returnPong;
app.get(() => ({ path: "/ping", use: use1 }));
// Return an object:
// content-type: application/json
const use = () => ({ resp: () => ({}) });
// Set type and status
const use = () => ({ resp: () => "pong", status: 201, type: "text/plain" });
Named params
Tino uses named params for defining your routes, like:
app.get(() => ({ path: "/users/:id", use }));
Props
Any controller can receive props:
const use = (props) => ({ resp: props.params.id });
app.post(() => ({ path: "/users/:id", use }));
Prop type
Prop is an object with following attributes:
- body - body sent from POST, PUT and PATCH methods
- params - parameters from named params
- query - query object from string like ?p=1&q=2
- custom params
- matchedPath - path regex
- pathPattern - path definition
- req - as
{ method, url }
- Any other parameters coming from middlewares
Middlewares
Middlewares are provided through async composition of functions. Each function in chain must return properties which are required for next function in chain, or which are required to pass to controller at the end of the chain.
Let's say we have two async functions and one sync, chained together:
// first we need `useMiddlewares` helper for composition
import { withMiddlewares } from "./tino.js";
const withAuth = async (props) => ({ currentUser: {}, userData: props.body });
const isAdmin = ({ currentUser }) => ({ isAdmin: false, currentUser });
const withDB = async (props) => ({ coll: {}, ...props });
// Then we chain(compose) them:
const composed = useMiddlewares(
withAuth,
isAdmin,
withDB,
);
// Then we wrap our controller with it:
const use = composed((props) => ({ resp: props.currentUser }));
app.get(() => ({ path: "/myapi", use }));
Throw exceptions in middlewares (protect routes)
If you want to return early from middleware chain, just throw an exception with same definition as controller's { resp, status?, type? }
:
const withAuth = async () => { throw { resp: "Boom", status: 401 } };
So whatever you return from your controller, your endpoint result will be:
HTTP/1.1 401 Unauthorized
content-length: 4
content-type: text/plain
Boom
Responders
Responders are different set of functions which are useful for writing your own namespaced endpoints or let other people use your package with Tino.
To define one just add root: true
param to endpoint definition:
app.any(() => ({ path: "/api/v2", use: myAPI.v2, root: true }));
.any
stands for any of the HTTP methods so your namespace reacts to all of them.
Your myAPI.v2
function will receive ctx
object which contains some Deno stuff like:
{
req: ServerRequest,
body,
query,
params,
use, // reference to your function
}
jsondb
responder
This responder comes built-in with Tino. It opens /api
path by default and is responsible for restful CRUD operations against local db.json
file. To learn more about it check it out: https://github.com/Vertrical/tino/blob/develop/README.md#using-jsondb-responder.
Thank you for reading this far about tino and I hope you liked it.
Again, to see how tino can be used check out maintained examples. Tino is under heavy development and expect more to come and more articles. ;)
Cheers! 🍻
Top comments (0)