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.

rulesresponses.md

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

Responses

Custom responses in api/responses/ define how actions communicate results to the client. The Boring Stack uses Inertia-aware response types for pages, validation, redirects, and status pages.

Response Files

api/responses/
├── inertia.js          ← Renders an Inertia page
├── inertia-redirect.js ← 409 full page reload (for stale once() data)
├── badRequest.js       ← Validation errors → session → redirect back
├── serverError.js      ← 500 handling with Youch in dev, status page in prod
├── notFound.js         ← 404 status page
└── forbidden.js        ← 403 status page

inertia.js -- Render a Page

// api/responses/inertia.js
module.exports = function inertia(data) {
  return this.req._sails.inertia.render(this.req, this.res, data)
}

Used when an action returns page data:

// In an action:
exits: { success: { responseType: 'inertia' } },
fn: async function () {
  return {
    page: 'dashboard/index',
    props: { user, teams }
  }
}

inertia-redirect.js -- Full Page Reload

// api/responses/inertia-redirect.js
module.exports = function inertiaRedirect(url) {
  return this.req._sails.inertia.location(this.req, this.res, url)
}

Sends a 409 Conflict response with X-Inertia-Location header, causing the client to do a full page reload. Used when cached once() data has changed:

// In an action:
exits: { success: { responseType: 'inertiaRedirect' } },
fn: async function () {
  sails.inertia.refreshOnce('loggedInUser')
  return '/dashboard'
}

badRequest.js -- Validation Errors

// api/responses/badRequest.js
module.exports = function badRequest(data) {
  return this.req._sails.inertia.handleBadRequest(this.req, this.res, data)
}

This response:

  1. Parses problems array into { fieldName: 'message' } format
  2. Stores errors in req.session.errors
  3. Redirects back to the Referrer URL with 303
  4. On next page load, the Inertia middleware shares errors as the errors prop
// In an action:
exits: {
  badSignupRequest: { responseType: 'badRequest' }
},
fn: async function ({ email }) {
  throw {
    badSignupRequest: {
      problems: [
        { email: 'An account with this email already exists.' }
      ]
    }
  }
}

The Problems Format

// Object format (preferred) -- maps to form.errors.fieldName
problems: [
  { email: 'Email is already taken.' },
  { password: 'Password must be at least 8 characters.' }
]

// String format (from Sails validation) -- field name extracted from quotes
problems: ['"email" must be a valid email address.']
// → form.errors.email = 'must be a valid email address.'

// General error (non-field)
problems: [{ login: 'Wrong email/password combination.' }]
// → form.errors.login = 'Wrong email/password combination.'

serverError.js, notFound.js, forbidden.js -- Error Handling

// api/responses/serverError.js
module.exports = function serverError(data) {
  return this.req._sails.inertia.handleServerError(this.req, this.res, data)
}

// api/responses/notFound.js
module.exports = function notFound(error) {
  return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
    statusCode: 404,
    error
  })
}

// api/responses/forbidden.js
module.exports = function forbidden(error) {
  return this.req._sails.inertia.handleErrorPage(this.req, this.res, {
    statusCode: 403,
    error
  })
}

In development: 500-level HTML responses render a Youch error page. Inertia visits show that page in the Inertia development modal.

In production: 403, 404, 500, and 503 render the configured Inertia status page by default:

// config/inertia.js
module.exports.inertia = {
  errorPage: 'error',
  errorStatuses: [403, 404, 500, 503]
}

Ship assets/js/pages/error.{vue,jsx,svelte} in the client. Hybrid apps that prefer views/404.ejs and views/500.ejs can set errorPage: false.

Generating Response Files

npx sails generate inertia              # Creates api/responses/inertia.js
npx sails generate inertia-redirect     # Creates api/responses/inertia-redirect.js
npx sails generate bad-request          # Creates api/responses/badRequest.js

Content-Negotiating Custom Responses

Custom responses can return different formats based on whether the request wants JSON (API/AJAX) or HTML (browser):

unauthorized.js

// api/responses/unauthorized.js
module.exports = function unauthorized() {
  var req = this.req
  var res = this.res

  if (req.wantsJSON) {
    return res.sendStatus(401)
  }

  // Clear the session and redirect to login
  if (req.session.userId) {
    delete req.session.userId
  }
  return res.redirect('/login')
}

expired.js (Token Expired)

// api/responses/expired.js
module.exports = function expired() {
  var req = this.req
  var res = this.res

  if (req.wantsJSON) {
    return res.status(498).send('Token Expired/Invalid')
  }
  return res.status(498).view('498')
}

badConfig.js (Missing Configuration)

A response that provides helpful troubleshooting guidance:

// api/responses/badConfig.js
module.exports = function badConfig(configKeyPath) {
  let res = this.res

  let explanation = 'Missing, incomplete, or invalid configuration'
  if (configKeyPath) {
    explanation += ` (sails.config.${configKeyPath}).`
    if (configKeyPath.match(/^builtStaticContent/)) {
      explanation +=
        ' Try running `sails run build-static-content`, then re-lift the server.'
    } else {
      explanation += ' Update this configuration, then re-lift the server.'
    }
  }

  return res.serverError(explanation)
}

// Usage in an action:
// exits: { badConfig: { responseType: 'badConfig' } }
// throw { badConfig: 'builtStaticContent.queries' }

Built-in Sails Responses

These are available without custom response files:

// In classic actions (not actions2):
res.ok(data) // 200
res.badRequest(data) // 400
res.forbidden() // 403
res.notFound() // 404
res.serverError(err) // 500
res.redirect(url) // 302
res.redirect(301, url) // 301
res.json(data) // JSON response
res.send(data) // Auto-detect content type
res.status(201).json() // Chain status + response

API / JSON Responses

For API endpoints or webhooks that don't use Inertia:

// api/controllers/api/get-status.js
module.exports = {
  fn: async function () {
    // Returning an object without responseType sends JSON
    return {
      status: 'healthy',
      version: '1.0.0',
      uptime: process.uptime()
    }
  }
}
// Or with explicit status code:
fn: async function () {
  this.res.status(201)
  return { id: newRecord.id, created: true }
}

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