All skills

Sails.js framework patterns for The Boring JavaScript Stack - actions, helpers, routes, policies, hooks, configuration, security, middleware, file uploads, deployment, and more. Use this skill when building, reviewing, or debugging any server-side code in a Sails.js application.

Use this Skill: https://skilld.dev/gh/sailscastshq/boring-stack/sails

This session only. Nothing lands on disk.

rulesroutes.md

≈2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Routes

Routes map URLs to actions. All routes are defined in config/routes.js.

Basic Route Syntax

// config/routes.js
module.exports.routes = {
  // 'VERB /path': { action: 'folder/action-name' }
  'GET /': { action: 'view-homepage' },
  'GET /login': { action: 'auth/view-login' },
  'POST /login': { action: 'auth/login' },
  'GET /signup': { action: 'auth/view-signup' },
  'POST /signup': { action: 'auth/signup' },
  'DELETE /logout': { action: 'auth/logout' },
  'GET /dashboard': { action: 'dashboard/view-dashboard' }
}

HTTP Verbs

Verb Use Case
GET Render pages, fetch data
POST Create resources
PATCH Update resources (partial)
PUT Replace resources (full)
DELETE Remove resources

If no verb is specified, the route matches all HTTP methods:

'/webhook': { action: 'webhooks/receive' }  // Matches GET, POST, PUT, PATCH, DELETE

Dynamic Parameters

module.exports.routes = {
  // Named parameter -- available as input or req.param('id')
  'GET /teams/:id': { action: 'team/view-team' },
  'PATCH /teams/:id': { action: 'team/update-team' },
  'DELETE /teams/:id': { action: 'team/delete-team' },

  // Multiple parameters
  'GET /teams/:teamId/members/:memberId': { action: 'team/view-member' },

  // Optional parameter
  'GET /blog/:slug?': { action: 'blog/view-post' }
}

In the action, dynamic parameters are available as inputs:

// api/controllers/team/view-team.js
module.exports = {
  inputs: {
    id: { type: 'string', required: true } // From :id in the route
  },
  exits: {
    success: { responseType: 'inertia' }
  },
  fn: async function ({ id }) {
    const team = await Team.findOne({ id })
    if (!team) throw 'notFound'
    return { page: 'teams/show', props: { team } }
  }
}

Wildcard Routes

module.exports.routes = {
  // Catch-all wildcard -- matches everything after the prefix
  'GET /docs/*': { action: 'docs/view-page' },

  // Global catch-all (place LAST -- used for 404 pages)
  'GET /*': { action: 'not-found', skipAssets: true }
}

Access wildcard values with urlWildcardSuffix:

// api/controllers/docs/view-page.js
module.exports = {
  urlWildcardSuffix: 'path',
  inputs: {
    path: { type: 'string', defaultsTo: '' }
  },
  fn: async function ({ path }) {
    // path = 'getting-started/installation' for /docs/getting-started/installation
  }
}

A Complete Routes File (Boring Stack Pattern)

// config/routes.js
module.exports.routes = {
  //  ╔═╗╔═╗╦  ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
  //  ╠═╣╠═╝║  ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
  //  ╩ ╩╩  ╩  ╚═╝╝╚╝═╩╝╩  ╚═╝╩╝╚╝ ╩ ╚═╝

  // Webhooks
  'POST /webhooks/stripe': { action: 'webhooks/receive-stripe' },

  //  ╦ ╦╔═╗╔╗ ╔═╗╔═╗╔═╗╔═╗╔═╗
  //  ║║║║╣ ╠╩╗╠═╝╠═╣║ ╦║╣ ╚═╗
  //  ╚╩╝╚═╝╚═╝╩  ╩ ╩╚═╝╚═╝╚═╝

  // Auth
  'GET /login': { action: 'auth/view-login' },
  'GET /signup': { action: 'auth/view-signup' },
  'POST /login': { action: 'auth/login' },
  'POST /signup': { action: 'auth/signup' },
  'DELETE /logout': { action: 'auth/logout' },

  // Password reset
  'GET /forgot-password': { action: 'auth/view-forgot-password' },
  'POST /forgot-password': { action: 'auth/send-password-reset' },
  'GET /reset-password/:token': { action: 'auth/view-reset-password' },
  'POST /reset-password': { action: 'auth/reset-password' },

  // Dashboard
  'GET /': { action: 'dashboard/view-homepage' },
  'GET /dashboard': { action: 'dashboard/view-dashboard' },

  // Profile / Settings
  'GET /profile': { action: 'dashboard/view-profile' },
  'PATCH /profile': { action: 'settings/update-profile' },
  'PATCH /settings/password': { action: 'settings/update-password' },
  'DELETE /profile': { action: 'settings/delete-account' },

  // Teams
  'GET /teams': { action: 'team/view-teams' },
  'GET /teams/:id': { action: 'team/view-team' },
  'POST /teams': { action: 'team/create-team' },
  'PATCH /teams/:id': { action: 'team/update-team' },
  'DELETE /teams/:id': { action: 'team/delete-team' },

  //  ╔╦╗╦╔═╗╔═╗
  //  ║║║║╚═╗║
  //  ╩ ╩╩╚═╝╚═╝

  // 404 catch-all (must be last)
  'GET /*': { action: 'not-found', skipAssets: true }
}

Route Target Types

Action Target (most common)

'GET /dashboard': { action: 'dashboard/view-dashboard' }

// Shorthand (without object wrapper)
'GET /dashboard': 'dashboard/view-dashboard'

View Target (static pages, no action needed)

'GET /terms':   { view: 'legal/terms' }       // Renders views/legal/terms.ejs
'GET /privacy': { view: 'legal/privacy' }

Redirect Target

'/old-dashboard':  '/dashboard',              // Internal redirect
'/docs':           'https://docs.example.com' // External redirect

Function Target (inline handler)

'GET /health': function (req, res) {
  return res.json({ status: 'ok', uptime: process.uptime() })
}

Policy + Action Chain

'GET /admin': [
  { policy: 'is-admin' },
  { action: 'admin/view-dashboard' }
]

Route Options

'GET /*': {
  action: 'not-found',
  skipAssets: true,     // Don't match URLs with dots (images, CSS, JS, etc.)
}

'GET /api/*': {
  action: 'api/not-found',
  csrf: false,          // Disable CSRF for this route
}
Option Description
skipAssets Don't match URLs containing dots (e.g., style.css, logo.png)
skipRegex RegExp or array of RegExps to skip
csrf Override CSRF protection for this route (true or false)
cors Override CORS settings for this route
locals Default view locals for this route

Using locals for Page Metadata

Set view locals directly in the route definition for SEO metadata and layout configuration:

'GET /pricing': {
  action: 'view-pricing',
  locals: {
    currentSection: 'pricing',
    pageTitleForMeta: 'Pricing',
    pageDescriptionForMeta: 'Plans and pricing for Fleet.'
  }
},

'GET /': {
  action: 'view-homepage',
  locals: {
    isHomepage: true,
    showHeaderCTA: true,
  }
},

'GET /contact': {
  action: 'view-contact',
  locals: {
    pageTitleForMeta: 'Contact us',
    pageDescriptionForMeta: 'Get in touch with our team.',
    hideFooterLinks: true,
  }
}

These locals are available as res.locals.* in the action and view templates (or shared via Inertia props in the custom hook).

skipAssets: false for Wildcard Content Routes

When wildcard routes handle dynamic content paths that might conflict with static asset detection:

'GET /articles/*': {
  skipAssets: false,   // Allow URLs like /articles/fleet-4.0-release
  action: 'articles/view-basic-article',
  locals: { currentSection: 'more' }
}

Route Ordering

Sails sorts routes by specificity:

  1. Static paths first (/login, /dashboard)
  2. Dynamic parameters next (/teams/:id)
  3. Wildcards last (/*)

Within each group, routes are matched in the order they appear in config/routes.js. Always place your catch-all /* route last.

Source: SKILL.md on GitHub

1 alert17d4 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill provides comprehensive documentation and coding patterns for the Sails.js framework as used in The Boring JavaScript Stack. It includes detailed guides on application anatomy, security best practices, and production deployment. No malicious patterns or security vulnerabilities were detected.

  • Socket17d

    2 alerts: gptSecurity

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    13/20 files flagged

Signed by skilld at bf19e10. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 8 months ago
Other metadata
metadata
{
  "author": "sailscastshq",
  "version": "2.1.0",
  "tags": "sails, sailsjs, backend, mvc, actions, helpers, routes, policies, hooks, middleware, deployment, boring-stack"
}

README badge

README badge for sailscastshq/boring-stack/sails