ESC
Type to search...
S
Soli Docs

Scaffold Generator

Quickly generate complete MVC resources with models, controllers, views, tests, and migrations.

Native / mobile generators

CommandCreates
soli generate devicesDevice registration
soli generate client …Native shells
soli generate app_linksDeep-link proofs
soli generate offlineOutbox sync

Basic Usage

Generate a scaffold for a resource:

Terminal
$ soli generate scaffold users
$ soli generate scaffold products name:string price:decimal

What Gets Generated

Running soli generate scaffold users creates:

Model
app/models/users_model.sl
Controller
app/controllers/users_controller.sl
Views
app/views/users/ (index, show, new, edit, _form)
Tests
tests/controllers/users_controller_spec.sl (controller E2E only — no model test file)
Migration
db/migrations/*create_users_*.sl
Routes
Auto-added to config/routes.sl

Field Types

Specify fields with name:type syntax:

Type HTML Input Example
string text name:string
text textarea content:text
email email (unique index) email:email
password password (unique index) password:password
integer number age:integer
float number price:float
boolean checkbox active:boolean
date date picker birthdate:date

Generated Code

Running soli generate scaffold users name:string email:text generates:

Model

app/models/users_model.sl
class Users < Model
  static
    # Fields
    # name (string)
    # email (text)

    # Validations (auto-generated)
    validates("name", { "presence": true })
    validates("email", { "presence": true })
  end

  before_save("normalize_fields")  # symbols also work: before_save(:normalize_fields)
end

Controller

app/controllers/users_controller.sl
class UsersController < Controller
  static
    this.layout = "application"
  end

  def index
    users = Users.all()
    render("users/index", {
      "title": "Users"
    })
  end

  def show
    user = Users.find(params["id"])
    render("users/show", {
      "title": "View User"
    })
  end

  def create
    permitted = this._permit_params(params)
    user = Users.create(permitted)
    if user["valid"] == true
      return redirect("/users")
    end
    render("users/new", {
      "title": "New User"
    })
  end

  def update
    id = params["id"]
    permitted = this._permit_params(params)
    Users.update(id, permitted)
    return redirect("/users")
  end

  def delete
    id = params["id"]
    Users.delete(id)
    return redirect("/users")
  end

  # Mass assignment protection
  def _permit_params(params)
    {
      "name": params["name"],
      "email": params["email"]
    }
  end
end

Views

Index View

app/views/users/index.html.slv
<div class="p-6">
  <h1 class="text-2xl font-bold">Users</h1>

  <table class="w-full">
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <% for user in users %>
      <tr>
        <td><%= user["id"] %></td>
        <td><%= user["name"] %></td>
        <td>
          <a href="/users/<%= user["id"] %>">Show</a>
          <a href="/users/<%= user["id"] %>/edit">Edit</a>
        </td>
      </tr>
      <% end %>
    </tbody>
  </table>

  <a href="/users/new">New User</a>
</div>

Form Partial (shared by new/edit)

app/views/users/_form.html.slv
<form action="/users" method="POST">
  <input type="text" name="name" value="<%= user["name"] %>">
  <input type="text" name="email" value="<%= user["email"] %>">
  <button type="submit">Submit</button>
  </form>

New View

app/views/users/new.html.slv
<h1 class="text-2xl font-bold mb-6">New User</h1>

<%= render("users/_form", { "user": user }) %>

<a href="/users" class="text-amber-400 hover:text-amber-300">← Back to Users</a>

Edit View

app/views/users/edit.html.slv
<h1 class="text-2xl font-bold mb-6">Edit User</h1>

<%= render("users/_form", { "user": user }) %>

<a href="/users" class="text-amber-400 hover:text-amber-300">← Back to Users</a>

Show View

app/views/users/show.html.slv
<div class="p-6">
  <h1 class="text-2xl font-bold mb-4">User Details</h1>

  <dl class="space-y-4">
    <div>
      <dt class="text-sm text-gray-400">ID</dt>
      <dd class="text-lg text-white"><%= user["id"] %></dd>
    </div>
    <div>
      <dt class="text-sm text-gray-400">Name</dt>
      <dd class="text-lg text-white"><%= user["name"] %></dd>
    </div>
  </dl>

  <a href="/users/<%= user["id"] %>/edit" class="bg-yellow-600 text-white px-4 py-2 rounded">Edit</a>
  <a href="/users" class="text-amber-400 ml-4">← Back</a>
</div>

Migration

db/migrations/*create_users_*.sl
def up(db)
  db.create_collection("users")
end

def down(db)
  db.drop_collection("users")
end

Tests

The generator writes one spec: the controller E2E file below. There is no model spec — add one yourself (tests/users_model_spec.sl) if you want model-level coverage. It would look like this:

tests/users_model_spec.sl (write it yourself)
describe("UsersModel") do
  test("creates a valid record") do
    result = Users.create({ "name": "Test User" })
    assert(result["valid"])
  end
end

Controller Test

tests/controllers/users_controller_spec.sl (generated)
describe("UsersController") do
  before_each() do
    as_guest()
  end

  describe("GET /users") do
    test("returns list of users") do
      response = get("/users")
      assert_eq(res_status(response), 200)
    end

    test("renders with correct view assigns") do
      response = get("/users")
      assert(render_template())
      assert_eq(view_path(), "users/index.html")
      data = assigns()
      assert_hash_has_key(data, "users")
    end
  end

  describe("GET /users/:id") do
    test("returns 404 for missing record") do
      response = get("/users/99999")
      assert_eq(res_status(response), 404)
    end
  end

  describe("POST /users") do
    test("creates new record with valid data") do
      response = post("/users", {"name": "Test user"})
      assert_eq(res_status(response), 302)
    end

    test("rejects invalid data") do
      response = post("/users", {})
      assert_eq(res_status(response), 422)
    end
  end
end

Auto-Validations

Fields with types string, text, email, password, and url automatically get presence: true validation:

app/models/users_model.sl
class Users < Model
  static
    # Fields
    # name (string)
    # email (email)

    # Validations (auto-generated)
    validates("name", { "presence": true })
    validates("email", { "presence": true })
  end

  before_save("normalize_fields")  # symbols also work: before_save(:normalize_fields)
end

Mass Assignment Protection

Scaffolded controllers include built-in mass assignment protection to prevent security vulnerabilities. The _permit_params method whitelists only the allowed fields:

Security Pattern
class UsersController < Controller
  # POST /users
  def create
    # Only permit whitelisted fields
    permitted = this._permit_params(params)
    user = Users.create(permitted)
  end

  # Mass assignment protection
  def _permit_params(params)
    {
      "name": params["name"],
      "email": params["email"]
    }
  end
end

Security Best Practice

  • Never pass raw req.params directly to model operations
  • Always use _permit_params() to filter input
  • Add sensitive fields (is_admin, role, permissions) to the blacklist if needed

Generated Routes

Scaffold automatically adds RESTful routes:

HTTP Path Action
GET /users index
GET /users/new new
POST /users create
GET /users/:id show
GET /users/:id/edit edit
PUT /users/:id update
DELETE /users/:id delete

Example

Generate a complete blog posts resource:

Terminal
$ soli generate scaffold posts title:string content:text author:string published:boolean
Success! Created scaffold for posts

Next Steps

  • Collections are auto-created - no migration needed!
  • Start server: soli serve . --dev
  • Visit: http://localhost:5011/posts
  • Tip: Use migrations to define indexes for better query performance in production.