-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7f9a5e9
commit 97635a8
Showing
17 changed files
with
493 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,242 @@ | ||
# roarter | ||
# Roarter | ||
|
||
A minimalist, intuitive, and unobtrusive middleware framework for Deno. | ||
|
||
The goal of Roarter is to only add what is absolutely necessary to the native | ||
HTTP server API, with as few abstractions as we can get away with. | ||
|
||
# Handlers | ||
|
||
The `Application` class is responsible for coordinating an HTTP request's | ||
interaction with handlers. You may register a new `Handler` via | ||
`Application.handle()` as shown below. | ||
|
||
```typescript | ||
let app = new Application(); | ||
|
||
app | ||
.handle(async (req) => { | ||
return Response.text("Hello World"); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); | ||
``` | ||
|
||
If you run the script | ||
|
||
```shell | ||
deno run --allow-net helloWorld.ts | ||
``` | ||
|
||
all requests made to `localhost:8080` will return `Hello World`. This is because | ||
we have not specified any `Matchers` for our `Handler`, so it will match all/any | ||
incoming requests. | ||
|
||
> NOTE: Handlers must be async (return a promise). | ||
> | ||
> This is to allow Roarter to orchestrate the HTTP request among middlewares and | ||
> handlers. | ||
# Matchers | ||
|
||
By adding `Matchers` we can make it so the `Handler` is only executed if the | ||
given conditions are met. For example, if we want our above example to only | ||
respond `Hello World` if the request has an HTTP verb of `GET`, we would do the | ||
following: | ||
|
||
```typescript | ||
let app = new Application(); | ||
|
||
app | ||
.match((req) => req.method === "GET") | ||
.handle(async (req) => { | ||
return Response.text("Hello World"); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); | ||
``` | ||
|
||
`Matchers` are passed the `Request` object and are expected to return boolean. | ||
If all matchers return `true`, then `Application` will run the `Handler`. | ||
|
||
In practice, Roarter includes most of the matchers you would ever need, so the | ||
above example may be written with the `.get` matcher that's already included in | ||
the framework. | ||
|
||
```typescript | ||
app | ||
.get | ||
.handle(async (req) => { | ||
return Response.text("Hello World"); | ||
}); | ||
``` | ||
|
||
> Matchers may also pass information down to their handler which may be accessed | ||
> via `req.params` or `req.vars`. | ||
> | ||
> This is useful when creating matchers that must "capture" values and pass them | ||
> down. An example of this is the `.path("/user/:userId")` matcher which has to | ||
> capture the value passed into `:userId`. | ||
# Params | ||
|
||
The `path()` matcher can be used to match a pathname and, optionally, capture | ||
params. It stores all path params under the `req.params` | ||
[Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). | ||
|
||
```typescript | ||
app | ||
.get | ||
.path("/user/:userId") | ||
.handle(async (req) => { | ||
return new Response("Echoing param " + req.params.get("userId")); | ||
}); | ||
``` | ||
|
||
# Sub Applications | ||
|
||
As your application gets larger you will want to logically organize your | ||
handlers. Roarter supports sub-routing to meet this need. Simply pass an | ||
instance of `Application` to the `.handle()` method and it will treat it as a | ||
sub-router. | ||
|
||
```typescript | ||
let user = new Application(); | ||
|
||
/** | ||
* GET tenant/:tenantId/user/:userId | ||
*/ | ||
user | ||
.get | ||
.path("/user/:userId") | ||
.handle(async (req) => { | ||
return new Response( | ||
`Echoing params ${req.params.get("tenantId")}, ${ | ||
req.params.get("userId") | ||
}`, | ||
); | ||
}); | ||
|
||
let tenant = new Application(); | ||
|
||
/** | ||
* GET tenant/:tenantId | ||
*/ | ||
tenant | ||
.path("/tenant/:tenantId") | ||
.handle(user); | ||
|
||
await tenant.serve({ port: 8080 }); | ||
``` | ||
|
||
# Errors | ||
|
||
Roarter `Applications` can handle errors within the handler itself: | ||
|
||
```typescript | ||
app | ||
.post | ||
.path("/json") | ||
.handle(async (req) => { | ||
if (!req.body) { | ||
return Response.text("no body", { status: 400 }); | ||
} | ||
}); | ||
``` | ||
|
||
Or via `Application.catch()`: | ||
|
||
```typescript | ||
app.catch(async (req, err) => { | ||
return Response.text(err.message, { status: 500 }); | ||
}); | ||
``` | ||
|
||
> `.catch()` gets called if any handlers throw an exception. It is always a good | ||
> idea to add one in production. | ||
# The Request and Response Objects | ||
|
||
Roarter's `Request` and `Response` objects extend the native | ||
[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and | ||
[Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects. | ||
Our goal is for our users to familiarize themselves with de native api as much | ||
as possible. That said, we are not against adding practical utility methods when | ||
needed. Below is a practical example of receiving and responding JSON. | ||
|
||
```typescript | ||
let app = new Application(); | ||
|
||
/** | ||
* GET /json | ||
*/ | ||
app | ||
.get | ||
.path("/json") | ||
.handle(async (req) => { | ||
const response = { | ||
id: "123", | ||
data: [1, 2, 3], | ||
}; | ||
|
||
return Response.json(response); | ||
}); | ||
|
||
/** | ||
* POST /json | ||
*/ | ||
app | ||
.post | ||
.path("/json") | ||
.handle(async (req) => { | ||
if (!req.body) { | ||
return Response.text("no body", { status: 400 }); | ||
} | ||
|
||
const body = await req.json(); | ||
|
||
return Response.json(body); | ||
}); | ||
|
||
app.catch(async (req, err) => { | ||
return Response.text(err.message, { status: 500 }); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); | ||
``` | ||
|
||
# Middleware | ||
|
||
A Handler with no Matchers is essentially a Middleware. Roarter will run all | ||
matching Handlers in order of insertion, so a Middleware that is added before a | ||
Handler will run first, the Handler will run after that, and, finally, the | ||
remaining Middleware if any. | ||
|
||
```typescript | ||
let app = new Application(); | ||
|
||
const jsonParser = async (req: Request) => { | ||
if (req.body) { | ||
req.vars.set("body", await req.json()); | ||
} | ||
}; | ||
|
||
const logger = async (req: Request) => { | ||
console.log(`Request sent to ${req.pathname}`); | ||
}; | ||
|
||
app | ||
.handle(jsonParser); | ||
|
||
app | ||
.post | ||
.path("/json") | ||
.handle(async (req) => { | ||
return Response.json(req.vars.get("body")); | ||
}); | ||
|
||
app | ||
.handle(logger); | ||
|
||
await app.serve({ port: 8080 }); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
import { serve, ServeInit } from "https://deno.land/[email protected]/http/server.ts"; | ||
import { Handler } from "./handler.ts"; | ||
import {CatchFunc, HandleFunc, Matcher} from "./types.ts"; | ||
import { CatchFunc, HandleFunc, Matcher } from "./types.ts"; | ||
import { Request as RoarterRequest } from "./request.ts"; | ||
|
||
export class Application { | ||
handlers: Handler[] = []; | ||
catchFunc: CatchFunc = () => { | ||
private handlers: Handler[] = []; | ||
private catchFunc: CatchFunc = () => { | ||
throw new Error("Not Implemented"); | ||
}; | ||
|
||
|
@@ -63,30 +63,30 @@ export class Application { | |
return h; | ||
} | ||
|
||
catch(fn: CatchFunc) { | ||
catch(fn: CatchFunc): void { | ||
this.catchFunc = fn; | ||
} | ||
|
||
// Handler runs all the registered handlers and returns the first | ||
// response it finds. After returning the first response, it runs | ||
// the remaining handlers. The purpose of this is to support middleware | ||
// after a response has been sent. | ||
async runHandlers(req: RoarterRequest): Promise<Response> { | ||
let runRemaining = true | ||
private async runHandlers(req: RoarterRequest): Promise<Response> { | ||
let runRemaining = true; | ||
let i = 0; | ||
try { | ||
try { | ||
for (i; i < this.handlers.length; i++) { | ||
const handler = this.handlers[i]; | ||
const response = await handler.run(req); | ||
const response = await handler["run"](req); | ||
if (response) { | ||
i++; | ||
return response; | ||
} | ||
} | ||
} catch (e) { | ||
// If an error occurs, we wan't to skip all handlers and run the catchFunc | ||
runRemaining = false | ||
runRemaining = false; | ||
return await this.catchFunc(req, e); | ||
} | ||
// If after running all the handlers there is no response, throw the error | ||
|
@@ -99,17 +99,17 @@ export class Application { | |
// we run the remaining handlers ignoring their response and errors | ||
for (i; i < this.handlers.length; i++) { | ||
const handler = this.handlers[i]; | ||
handler.run(req).catch(e => { | ||
this.catchFunc(req, e) | ||
}) | ||
handler["run"](req).catch((e) => { | ||
this.catchFunc(req, e); | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
|
||
async handler(domReq: Request): Promise<Response> { | ||
private async handler(domReq: Request): Promise<Response> { | ||
const req = new RoarterRequest(domReq); | ||
return this.runHandlers(req) | ||
return this.runHandlers(req); | ||
} | ||
|
||
async serve(options?: ServeInit) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Application, Response } from "../../mod.ts"; | ||
|
||
let app = new Application(); | ||
|
||
app | ||
.get | ||
.path("/") | ||
.handle(async (req) => { | ||
return Response.text("Hello World"); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { Application, Response } from "../../mod.ts"; | ||
|
||
let app = new Application(); | ||
|
||
/** | ||
* GET /json | ||
*/ | ||
app | ||
.get | ||
.path("/json") | ||
.handle(async (req) => { | ||
const response = { | ||
id: "123", | ||
data: [1, 2, 3], | ||
}; | ||
|
||
return Response.json(response); | ||
}); | ||
|
||
/** | ||
* POST /json | ||
*/ | ||
app | ||
.post | ||
.path("/json") | ||
.handle(async (req) => { | ||
if (!req.body) { | ||
return Response.text("no body", { status: 400 }); | ||
} | ||
|
||
const body = await req.json(); | ||
|
||
return Response.json(body); | ||
}); | ||
|
||
app.catch(async (req, err) => { | ||
return Response.text(err.message, { status: 500 }); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Application, Response } from "../../mod.ts"; | ||
|
||
let app = new Application(); | ||
|
||
app | ||
.match((req) => req.method === "GET") | ||
.handle(async (req) => { | ||
return Response.text("Hello World"); | ||
}); | ||
|
||
await app.serve({ port: 8080 }); |
Oops, something went wrong.