{"id":19142245,"url":"https://github.com/eldoy/sirloin","last_synced_at":"2025-05-06T23:45:55.986Z","repository":{"id":57361936,"uuid":"152062622","full_name":"eldoy/sirloin","owner":"eldoy","description":"Node web server for HTTP, web sockets and static files.","archived":false,"fork":false,"pushed_at":"2022-04-08T09:13:57.000Z","size":4744,"stargazers_count":7,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-19T16:11:31.949Z","etag":null,"topics":["api","fast","file-server","http","http-server","https","loadbalancer","microservice","node","proxy","pubsub","static","webserver","websocket"],"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/eldoy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-10-08T10:37:21.000Z","updated_at":"2023-12-20T18:49:42.000Z","dependencies_parsed_at":"2022-09-18T05:57:19.578Z","dependency_job_id":null,"html_url":"https://github.com/eldoy/sirloin","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldoy%2Fsirloin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldoy%2Fsirloin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldoy%2Fsirloin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldoy%2Fsirloin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eldoy","download_url":"https://codeload.github.com/eldoy/sirloin/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252788404,"owners_count":21804280,"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":["api","fast","file-server","http","http-server","https","loadbalancer","microservice","node","proxy","pubsub","static","webserver","websocket"],"created_at":"2024-11-09T07:26:29.919Z","updated_at":"2025-05-06T23:45:55.968Z","avatar_url":"https://github.com/eldoy.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Sirloin logo](https://s3.amazonaws.com/7ino/1539200413_sirloin-logo200x128.png)\n\n# Sirloin Node.js Web Server\n\nThis high performance easy to use web server includes:\n\n* HTTP server for your APIs and microservices\n* Support for file uploads and post body parsing\n* Fast and minimal, just a few hundred lines of code\n* Integrated websocket server based on actions\n* Static file server with compression support\n* Redis pubsub for scaling your websockets\n* Ping pong support terminating dead web sockets\n* Full async / await support\n* HTTPS over SSL support\n* Cookie handling\n\nZero configuration required, create an HTTP API endpoint with only 3 lines of code. If you're using websockets, the [wsrecon library](https://github.com/eldoy/wsrecon) is recommended as you'll get support for auto-reconnect and automatic JSON data handling out of the box.\n\nThe websockets are based on the excellent [ws library](https://github.com/websockets/ws), pubsub is based on [ioredis](https://github.com/luin/ioredis), and the rest is pure vanilla NodeJS.\n\n### Install\n```npm i sirloin```\n\n### HTTP Server\nSupported request methods are GET, POST, PUT, DELETE and PATCH. The response and request parameters are standard Node.js HTTP server Incoming and Outgoing message instances.\n\nThe default content type for GET requests is `text/html` and `application/json` for all other requests.\n\nThe router is just based on string lookup to make it really fast.\n```js\nconst sirloin = require('sirloin')\n\n// Default config shown\nconst server = sirloin({\n  // Web server port\n  port: 3000,\n\n  // Static files root directory\n  // Set to false to not serve static files\n  dir: 'dist',\n\n  // Redirect to this host if no match\n  host: 'https://example.com',\n\n  // Callback for websocket connect event\n  // Can be used for adding data to the websocket client\n  connect: async (client) =\u003e {},\n\n  // Redis pubsub is not enabled by default\n  pubsub: undefined,\n\n  // HTTPS over SSL support\n  ssl: {\n    key: '/path/to/server.key',\n    cert: '/path/to/server.crt'\n  }\n})\n\n// Get request, whatever you return will be the response\nserver.get('/db', async (req, res) =\u003e {\n  req.method       // Request method\n  req.path         // Request path\n  req.pathname     // Request path name\n  req.url          // Request URL\n  req.params       // Post body parameters\n  req.query        // Query parameters\n  req.files        // Uploaded files\n  req.cookie       // Cookie handler\n  return { hello: 'world' }\n})\n// See the documentation on Node.js 'incoming message' (req),\n// 'outgoing message' (res) and 'url' for more on what's available.\n\n// Post request, uploads must be post requests\nserver.post('/upload', async (req, res) =\u003e {\n  req.files // Array of uploaded files if any\n  return { success: true }\n})\n\n// Use the '*' for a catch all route\nserver.get('*', async (req, res) =\u003e {\n  if (req.path === '/custom') {\n    return { hello: 'custom' }\n  }\n  // Return nothing or undefined to send a 404\n})\n\n// Use 'all' to match all HTTP request methods\nserver.all('/all', async (req, res) =\u003e {\n  if (['POST', 'GET'].includes(req.method)) {\n    return { status: 'OK' }\n  }\n})\n\n// Use 'any' to match selected HTTP request methods\n// This matches 'post' and 'get' to the /any route\nserver.any('post', 'get', '/any', async (req, res) =\u003e {\n  return { status: 'OK' }\n})\n\n// You can also return HTML templates, strings, numbers and boolean values\nserver.get('/projects', async (req, res) =\u003e {\n  res.setHeader('Content-Type', 'text/html; charset=utf-8')\n  return '\u003ch1\u003eHello world\u003c/h1\u003e'\n})\n```\n\n### Middleware\nUse middleware to run a function before every request. You can return values from middleware as well and the rest of the middleware stack will be skipped.\n```js\n// Middleware functions are run in the order that they are added\nserver.use(async (req, res) =\u003e {\n  res.setHeader('Content-Type', 'text/html')\n\n  // Enable CORS\n  res.setHeader('Access-Control-Allow-Origin', '*')\n  res.setHeader('Access-Control-Allow-Credentials', 'true')\n  res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control')\n  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')\n})\n\n// Return directly from middleware to skip further processing\nserver.use(async (req, res) =\u003e {\n   const session = await db.session.find({ token: res.query.token })\n   if (!session) {\n     return { error: 'session not found' }\n   }\n})\n```\n\n### Websockets\nWebsockets are used through *actions*, the URL path is irrelevant. Include *$action: 'name'* in the data you are sending to the server to match your action. Connection handling through *ping and pong* will automatically terminate dead clients.\n\nWebsocket connections are lazy loaded and enabled only if you specify an action. All websocket actions must return Javascript objects (sent as JSON).\n```js\n// Websocket actions work like remote function calls\nserver.action('hello', async (data, client) =\u003e {\n  data             // The data sent from the browser minus action\n  client.id        // The id of this websocket client\n  client.send()    // Use this function to send messages back to the browser\n  client.req       // The request object used to connect to the websocket\n\n  // Return a javascript object to send to the client\n  return { hello: 'world' }\n})\n\n// Example socket client setup with wsrecon\nconst wsrecon = require('wsrecon')\nconst socket = await wsrecon('ws://example.com')\n\n// Normal socket send from the browser, matches the action named 'hello'\nsocket.send({ $action: 'hello' })\n\n// Get data from the web socket\nsocket.on('message', function(data) {\n  console.log(data) // { hello: 'world' }\n})\n\n// Define a '*' action to not use actions\nserver.action('*', async (data, client) =\u003e {\n  // Will send what you return\n  return { hello: 'custom' }\n})\n\n// The client send function supports callbacks and promises\nserver.action('promise', async (data, client) =\u003e {\n  // Unordered, the next line happens immediately\n  client.send({ hello: 'send' })\n\n  // With promise, the next line happens after this is done\n  await client.send({ hello: 'promise' })\n\n  // With callback, the next line happens immediately\n  await client.send({ hello: 'promise' }, () =\u003e {\n    console.log('In callback, sent it')\n  })\n\n  // ...\n})\n\n// All of the options in the ws library are supported for send\nserver.action('options', async (data, client) =\u003e {\n  await client.send({ message: 'terminated' }, { compress: true })\n\n  // Terminate the client when sending is done\n  client.terminate()\n\n  // ...\n})\n```\n\n### Redis Pubsub\nIf you have more than one app server for your websockets, you need pubsub to reliably publish messages to multiple clients. With pubsub, the messages go via a [Redis server](https://redis.io), a high performance key-value store.\n\nSirloin has built in support for pubsub, all you need to do is to [install Redis](https://redis.io/download) and enable it in your Sirloin config:\n```js\n// Default config options shown\nconst server = sirloin({\n  pubsub: {\n    port: 6379,          // Redis port\n    host: 'localhost',   // Redis host URL\n    path: null,          // Socket path\n    family: 4,           // 4 (IPv4) or 6 (IPv6)\n    password: null,      // Redis password\n    db: 0,               // Redis database\n    channel: 'messages'  // Subscription channel\n  }\n})\n\n// To use the default options, this is all you need\n// Make sure Redis is running before starting your application\nconst server = sirloin({ pubsub: true }) // or pubsub: {}\n\n// First subscribe to a function\nserver.subscribe('live', async (data, client) =\u003e {\n  // Publish data to all clients except publisher (client)\n  server.websocket.clients.forEach(c =\u003e {\n    if (client.id !== c.id) {\n      c.send(data)\n    }\n  })\n})\n\n// Use the 'publish' function to publish messages to multiple clients\nserver.action('publish', async (data, client) =\u003e {\n  // This will call the subscribed function named 'live' on every app server\n  client.publish('live', { hello: 'world' })\n\n  // Publish to all without client, in case you don't have it\n  server.pubsub.publish('live', { hello: 'all' })\n})\n\n// The publish function works with await\nawait client.publish('live', { hello: 'world' })\n\n// ... and callbacks\nclient.publish('live', { hello: 'world' }, () =\u003e {\n  // Publish is done, notify the publisher\n  client.send({ published: true })\n})\n```\nPubsub is disabled by default, remove the config or set to 'false' to send messages directly to the socket.\n\n### API \u0026 Configuration\nThe server object contains functions and properties that are useful as well:\n```js\nserver.http                        // The HTTP server reference\nserver.websocket                   // The Websocket server reference\nserver.websocket.clients           // The connected clients as an array\nserver.pubsub                      // The pubsub connection info\nserver.pubsub.channel              // The current pubsub channel name\nserver.pubsub.publisher            // The publishing pubsub connection\nserver.pubsub.subscriber           // The subscribing pubsub connection\nserver.config                      // The active config for the server\n\n// For each client you can send data to the browser\nserver.websocket.clients.forEach(client =\u003e {\n  client.send({ hello: 'world' })\n})\n\n// Find the client with the 'id' in id and send some data to it\nconst client = server.websocket.clients.find(c =\u003e c.id === id)\nclient.send({ data: { hello: 'found' } })\n```\n\n### Static File Server\nStatic files will be served from the 'dist' directory by default. Routes have presedence over static files. If the file path ends with just a '/', then the server will serve the 'index.html' file if it exists.\n```js\n// Set the static file directory via the 'dir' option, default is 'dist'\nconst server = sirloin({ dir: 'dist' })\n\n// Change it to the name of your static files directory\nconst server = sirloin({ dir: 'public' })\n\n// Set it to false to disable serving of static files\nconst server = sirloin({ dir: false })\n```\nIf the given directory doesn't exist static files will be disabled automatically.\n\nMime types are automatically added to each file to make the browser behave correctly. The server enables browser caching by using the Last-Modified header returning a 304 response if the file is fresh. This speeds up delivery a lot.\n\n### Error Handling\nErrors can be caught with ```try catch``` inside of middleware, routes and actions.\n```js\nserver.get('/crash', async (req, res) =\u003e {\n  try {\n    const user = await db.user.first()\n  } catch (e) {\n    console.log(e.message)\n    return { error: 'find user crashed' }\n  }\n})\n```\nYou can also collect errors in special routes and actions. The 'err' argument is a normal javascript Error instance.\n```js\n// For middleware and http routes use 'error'\nserver.error(async (err, req, res) =\u003e {\n  return { error: err.message }\n})\n\n// For websocket actions use 'fail'\nserver.fail(async (err, data, client) =\u003e {\n  return { error: err.message }\n})\n\n// Trigger error from middleware, will go to 'error' if defined\nserver.use(async (req, res) =\u003e {\n  throw new Error('middleware error!')\n})\n\n// Trigger error from http route, will go to 'error' if defined\nserver.post('/db', async (req, res) =\u003e {\n  throw new Error('http error!')\n})\n\n// Trigger error from websocket action, will go to 'fail' if defined\nserver.action('db', async (data, client) =\u003e {\n  throw new Error('websocket error!')\n})\n```\n\n### Examples of Use\nHere are a few examples showing how easy to use Sirloin can be:\n```js\n// File server running on port 3000 (yeah, only one line of code)\nrequire('sirloin')()\n\n// JSON API endpoint without routes (middleware only)\nconst server = require('sirloin')()\nserver.use(async (req, res) =\u003e {\n  return { hello: 'world' }\n})\n\n// JSON API endpoint with routes\nconst sirloin = require('sirloin')\nconst server = sirloin()\nserver.get('/', async (req, res) =\u003e {\n  return { hello: 'world' }\n})\n\n// JSON Websocket endpoint\nconst sirloin = require('sirloin')\nconst server = sirloin()\nserver.action('hello', async (data, client) =\u003e {\n  return { hello: 'world' }\n})\n```\nSee the [dev.js](https://github.com/eldoy/sirloin/blob/master/dev.js) file for more examples.\n\n### License\n\nMIT Licensed. Enjoy!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feldoy%2Fsirloin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feldoy%2Fsirloin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feldoy%2Fsirloin/lists"}