npm install @curveball/core
Every Curveball needs a Pitcher
import { Application } from '@curveball/core';
const app = new Application();
app.use( async ctx => {
ctx.response.type = 'text/plain';
ctx.response.body = 'hello world';
});
app.listen(80);
Everything is a middleware, and middlewares may or may not be async
.
import { Application } from '@curveball/core';
import { handler } from '@curveball/aws-lambda';
const app = new Application();
app.use( ctx => {
ctx.response.type = 'text/plain';
ctx.response.body = 'hello world';
});
exports.handler = handler(app);
To use Curveball with Bun, use the kernel package:
import { Application } from '@curveball/kernel';
const app = new Application();
// Add all your middlewares here!
app.use( ctx => {
ctx.response.body = {msg: 'hello world!'};
});
export default {
port: 3000,
fetch: app.fetch.bind(app)
};
Some more information and examples can be found in this article.
const app = new Application();
app.use( ctx => {
ctx.response.type = 'text/plain';
ctx.body = 'hello world';
ctx.push( pushCtx => {
pushCtx.path = '/sub-item';
pushCtx.response.type = 'text/html';
pushCtx.response.body = '<h1>Automatically pushed!</h1>';
});
});
The callback to ctx.push
will only get called if Push was supported by the
client, and because it creates a new ‘context’, any middleware can be attached
to it, or even all the middleware by doing a ‘sub request’.
Controllers are optional and opinionated. A single controller should only ever manage one type of resource, or one route.
import { Application, Context } from '@curveball/core';
import { Controller } from '@curveball/controller';
const app = new Application();
class MyController extends Controller {
get(ctx: Context) {
// This is automatically triggered for GET requests
}
put(ctx: Context) {
// This is automatically triggered for PUT requests
}
}
app.use(new MyController());
The recommended pattern is to use exactly one controller per route.
import { Application } from '@curveball/core';
import router from '@curveball/router';
const app = new Application();
app.use(router('/articles', new MyCollectionController());
app.use(router('/articles/:id', new MyItemController());
import { Context } from '@curveball/core';
import { Controller, method, accept } from '@curveball/controller';
class MyController extends Controller {
@accept('html')
@method('GET')
async getHTML(ctx: Context) {
// This is automatically triggered for GET requests with
// Accept: text/html
}
@accept('json')
@method('GET')
async getJSON(ctx: Context) {
// This is automatically triggered for GET requests with
// Accept: application/json
}
}
To emit an HTTP error, it’s possible to set ctx.status
, but easier to just
throw a related exception.
function myMiddleware(ctx: Context, next: Middleware) {
if (ctx.method !== 'GET') {
throw new MethodNotAllowed('Only GET is allowed here');
}
await next();
}
The project also ships with a middleware to automatically generate
RFC7807 application/problem+json
responses.
With express middlewares it’s easy to do something before a request was handled, but if you ever want to transform a response in a middleware, this can only be achieved through a complicated hack.
This is due to the fact that responses are immediately written to the TCP sockets, and once written to the socket it’s effectively gone.
So to do things like gzipping responses, Express middleware authors needs to mock the response stream and intercept any bytes sent to it. This can be clearly seen in the express-compression source: https://github.com/expressjs/compression/blob/master/index.js.
Curveball does not do this. Response bodies are buffered and available by middlewares.
For example, the following middleware looks for a HTTP Accept header of
text/html
and automatically transforms JSON to a simple HTML output:
app.use( async (ctx, next) => {
// Let the entire middleware stack run
await next();
// HTML encode JSON responses if the client was a browser.
if (ctx.accepts('text/html') && ctx.response.type ==== 'application/json') {
ctx.response.type = 'text/html';
ctx.response.body = '<h1>JSON source</h1><pre>' + JSON.stringify(ctx.response.body) + '</pre>';
}
});
To achieve the same thing in express would be quite complicated.
You might wonder if this is bad for performance for large files. You would be completely right, and this is not solved yet.
However, instead of writing directly to the output stream the intent for this
is to allow users to set a callback on the body
property, so writing the
body will not be buffered, just deferred. The complexity of implementing these
middlewares will not change.
Curveball also ships with an API browser that automatically transforms JSON in to traversable HTML, and automatically parses HAL links and HTTP Link headers.
Every navigation element is completely generated based on links found in the response.
To use it:
import { halBrowser } from 'hal-browser';
import { Application } from '@curveball/core';
const app = new Application();
app.use(halBrowser());
ctx.response.sendInformational(103, {
link: '</foo>; rel="preload"'
})
const foo = ctx.request.prefer('return');
// Could be 'representation', 'minimal' or false
console.log(foo);