Lightweight HTTP REST API framework with built-in hypermedia support
# npm
npm install tenso
# yarn
yarn add tenso
# pnpm
pnpm add tenso
import {tenso} from "tenso";
export const app = tenso();
app.get("/", "Hello, World!");
app.start();
import {tenso} from "tenso";
import {randomUUID as uuid} from "crypto";
const initRoutes = {
"get": {
"/": ["reports", "uuid"],
"/reports": ["tps"],
"/reports/tps": (req, res) => res.error(785, Error("TPS Cover Sheet not attached")),
"/uuid": (req, res) => res.send(uuid(), 200, {"cache-control": "no-cache"})
}
};
export const app = tenso({initRoutes});
app.start();
import {Tenso} from "tenso";
class MyAPI extends Tenso {
constructor() {
super({
auth: {
protect: ["/api/private"]
},
defaultHeaders: {
"x-api-version": "1.0.0"
}
});
this.setupRoutes();
}
setupRoutes() {
this.get("/api/health", this.healthCheck);
this.post("/api/users", this.createUser);
}
healthCheck(req, res) {
res.json({status: "healthy", timestamp: new Date().toISOString()});
}
createUser(req, res) {
// Handle user creation
res.status(201).json({message: "User created"});
}
}
const api = new MyAPI();
Routes are loaded as a module, with each HTTP method as an export, affording a very customizable API server.
You can use res to:
- res.send(body[, status, headers])
- res.redirect(url)
- res.error(status[, Error])
Protected routes require authorization for access and will redirect to authentication endpoints if needed.
Unprotected routes do not require authorization for access and will exit the authorization pipeline early to avoid rate limiting, CSRF tokens, & other security measures. These routes are the DMZ of your API! You must secure these endpoints with alternative methods if accepting input!
As of 17.2.0 you can have routes exit the middleware pipeline immediately by setting them in the exit Array. This differs from unprotect as there is no request body handling.
The /assets/* route is reserved for the HTML browsable interface assets; please do not try to reuse this for data.
// Middleware for all requests
export const always = {
"/": (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
}
};
// Protected routes with authentication
export const get = {
"/admin": (req, res) => {
// Only accessible after authentication
res.json({admin: true});
}
};
// Route parameters
export const get = {
"/users/:id": (req, res) => {
const userId = req.params.id;
res.json({id: userId});
}
};
Tenso decorates req with helpers such as:
- req.allow - Allowed HTTP methods
- req.csrf - CSRF token information
- req.ip - Client IP address
- req.parsed - Parsed URL information
- req.private - Private route flag
- req.body - Request payload for PATCH, PUT, & POST requests
- req.session - Session data when using local authentication
Tenso decorates res with helpers such as:
- res.send() - Send response with optional status and headers
- res.status() - Set response status
- res.json() - Send JSON response
- res.redirect() - Send redirect response
- res.error() - Send error response
Tenso is extensible and can be customized with custom parsers, renderers, & serializers.
Custom parsers can be registered with server.parser('mimetype', fn); or directly on server.parsers. The parameters for a parser are (arg).
Tenso has built-in parsers for:
- application/json
- application/x-www-form-urlencoded
- application/jsonl
- application/json-lines
- text/json-lines
Custom renderers can be registered with server.renderer('mimetype', fn);. The parameters for a renderer are (req, res, arg).
Tenso has built-in renderers for:
- application/javascript
- application/json
- application/jsonl
- application/json-lines
- text/json-lines
- application/yaml
- application/xml
- text/csv
- text/html
Custom serializers can be registered with server.serializer('mimetype', fn);. The parameters for a serializer are (arg, err, status = 200, stack = false).
Tenso has two default serializers which can be overridden:
- plain - for plain text responses
- custom - for standard response shape
{
"data": null,
"error": null,
"links": [],
"status": 200
}
Responses will have a standard shape and will be UTF-8 by default. The result will be in data. Hypermedia (pagination & links) will be in links: [{"uri": "...", "rel": "..."}, ...], & also in the Link HTTP header.
Page size can be specified via the page_size parameter, e.g. ?page_size=25.
Sort order can be specified via the order-by parameter which accepts [field ]asc|desc & can be combined like an SQL 'ORDER BY', e.g. ?order_by=desc or ?order_by=lastName%20asc&order_by=firstName%20asc&order_by=age%20desc
Hypermedia is a prerequisite of REST and is best described by the Richardson Maturity Model. Tenso will automatically paginate Arrays of results, or parse Entity representations for keys that imply relationships, and create the appropriate Objects in the link Array, as well as the Link HTTP header. Object keys that match this pattern: /_(guid|uuid|id|uri|url)$/ will be considered hypermedia links.
For example, if the key user_id was found, it would be mapped to /users/:id with a link rel of related.
Tenso will bend the rules of REST when using authentication strategies provided by passport.js, or CSRF if enabled, because they rely on a session. Session storage is in memory or Redis. You have the option of a stateless or stateful API.
Hypermedia processing of the response body can be disabled as of 10.2.0 by setting req.hypermedia = false and/or req.hypermediaHeader via middleware.
Tenso provides comprehensive configuration options to customize every aspect of your server. All configuration options are optional - provide only what you need to override the sensible defaults.
| Option | Type | Default | Description |
|---|---|---|---|
host |
string | "0.0.0.0" |
Server host address to bind to |
port |
number | 8000 |
Server port number to listen on |
title |
string | "tenso" |
Application title for branding and display |
version |
string | auto |
Framework version (auto-detected) |
silent |
boolean | false |
Suppress console output and logging |
maxListeners |
number | 25 |
Maximum number of event listeners |
| Option | Type | Default | Description |
|---|---|---|---|
mimeType |
string | "application/json" |
Default MIME type for responses |
charset |
string | "utf-8" |
Default character encoding for responses |
jsonIndent |
number | 0 |
JSON response indentation level (0 = minified) |
digit |
number | 3 |
Number of decimal places for numeric formatting |
renderHeaders |
boolean | true |
Include headers in rendered output responses |
time |
boolean | true |
Include timing information in response headers |
etags |
boolean | true |
Enable ETag generation for response caching |
{
defaultHeaders: {
"content-type": "application/json; charset=utf-8",
"vary": "accept, accept-encoding, accept-language, origin"
}
}
| Option | Type | Default | Description |
|---|---|---|---|
origins |
Array | ["*"] |
Allowed CORS origins |
corsExpose |
string | "cache-control, content-language, content-type, expires, last-modified, pragma" |
CORS exposed headers |
| Option | Type | Default | Description |
|---|---|---|---|
cacheSize |
number | 1000 |
Maximum number of items in memory cache |
cacheTTL |
number | 300000 |
Cache time-to-live in milliseconds (5 minutes) |
| Option | Type | Default | Description |
|---|---|---|---|
maxBytes |
number | 0 |
Maximum request body size in bytes (0 = unlimited) |
catchAll |
boolean | true |
Enable catch-all route handling for unmatched requests |
exit |
Array | [] |
Exit handlers to execute on server shutdown |
| Option | Type | Default | Description |
|---|---|---|---|
pageSize |
number | 5 |
Default pagination page size |
hypermedia.enabled |
boolean | true |
Enable hypermedia links in responses |
hypermedia.header |
boolean | true |
Include hypermedia links in response headers |
| Option | Type | Default | Description |
|---|---|---|---|
autoindex |
boolean | false |
Enable automatic directory indexing for static files |
webroot.root |
string | "www/" |
Document root directory for static files |
webroot.static |
string | "/assets" |
Static assets directory path |
webroot.template |
string | "template.html" |
Template file path for rendered responses |
The auth object controls all authentication-related settings:
{
auth: {
basic: {
enabled: false, // Enable basic authentication
list: [] // Array of "username:password" strings
}
}
}
{
auth: {
bearer: {
enabled: false, // Enable bearer token authentication
tokens: [] // Array of valid bearer tokens
}
}
}
{
auth: {
jwt: {
enabled: false, // Enable JWT authentication
auth: null, // Custom JWT authentication function
audience: "", // JWT audience claim
algorithms: [ // Allowed JWT signing algorithms
"HS256", "HS384", "HS512"
],
ignoreExpiration: false, // Ignore JWT expiration
issuer: "", // JWT issuer claim
scheme: "Bearer", // JWT authentication scheme
secretOrKey: "" // JWT secret or private key
}
}
}
{
auth: {
oauth2: {
enabled: false, // Enable OAuth2 authentication
auth: null, // Custom OAuth2 authentication function
auth_url: "", // OAuth2 authorization URL
token_url: "", // OAuth2 token URL
client_id: "", // OAuth2 client ID
client_secret: "" // OAuth2 client secret
}
}
}
{
auth: {
saml: {
enabled: false, // Enable SAML authentication
auth: null // Custom SAML authentication function
}
}
}
{
auth: {
delay: 0, // Authentication delay in milliseconds
protect: [], // Routes requiring authentication (regex patterns)
unprotect: [], // Routes excluded from authentication
uri: {
login: "/auth/login", // Login endpoint URI
logout: "/auth/logout", // Logout endpoint URI
redirect: "/", // Post-authentication redirect URI
root: "/auth" // Authentication root URI
},
msg: {
login: "POST 'username' & 'password' to authenticate"
}
}
}
$ claude mcp add tenso \
-- python -m otcore.mcp_server <graph>