We're going to build a tiny "links" app - a single page that lists URLs with titles, plus an API endpoint to post new ones. It exercises routing, controllers, an Eloquent model, a migration, and an Inertia page. If you can build this, you can build anything Suprnova does.
This assumes you've followed Installation and have
the suprnova CLI on your PATH.
1. Scaffold
Open http://127.0.0.1:8765. You should see the welcome page. Stop
the server (Ctrl+C) - we're going to add a feature.
2. Create the model and migration
There is no dedicated make:model command - models are regenerated
from the schema by db:sync --regenerate-models once the migration
runs. Start with the migration:
Open the new migration file under src/migrations/:
use *;
;
Create the model by hand at src/models/link.rs:
use ;
use ;
Add pub mod link; to src/models/mod.rs so the new module is
reachable, then apply the migration and regenerate entities:
db:sync runs pending migrations and regenerates SeaORM entities. The
combined step is the dev-loop default; in production you use plain
suprnova migrate.
3. Add a controller
src/controllers/link.rs:
use ;
use Validate;
use crateLink;
pub async
pub async
Register the controller module in src/controllers/mod.rs:
4. Wire the routes
src/routes.rs:
use ;
use cratecontrollers;
routes!
5. Build the Inertia page
Create frontend/src/pages/Links/Index.svelte (for the Svelte starter):
<script lang="ts">
import { router } from '@inertiajs/svelte';
let { links } = $props<{
links: { id: number; title: string; url: string }[]
}>();
let title = $state('');
let url = $state('');
function submit(e: SubmitEvent) {
e.preventDefault();
router.post('/links', { title, url }, {
onSuccess: () => { title = ''; url = ''; },
});
}
</script>
<div class="mx-auto max-w-2xl p-8">
<h1 class="text-2xl font-bold">Links</h1>
<form onsubmit={submit} class="mt-4 flex gap-2">
<input bind:value={title} placeholder="Title"
class="flex-1 rounded border p-2" />
<input bind:value={url} placeholder="https://..."
class="flex-1 rounded border p-2" />
<button class="rounded bg-blue-600 px-4 py-2 text-white">Add</button>
</form>
<ul class="mt-8 space-y-2">
{#each links as link}
<li class="rounded border p-3">
<a href={link.url} target="_blank"
class="text-blue-600 hover:underline">
{link.title}
</a>
<p class="text-sm text-gray-500">{link.url}</p>
</li>
{/each}
</ul>
</div>
(Equivalent React and Vue starters give you the same shape with their own templating - the Inertia bridge is identical.)
6. See it work
Visit http://127.0.0.1:8765/links. Add a couple of links via the form.
They post to /links, the controller writes to the links table, and
the Inertia request re-fetches the index props. No JSON marshalling
glue - InertiaProps derived the wire format for you.
What just happened
You touched eight files. Here's what they actually mean:
| File | Layer | Role |
|---|---|---|
src/migrations/m_create_links_table.rs |
Schema | Defines the links table |
src/models/link.rs |
Domain | One struct, four lines, full Eloquent model |
src/controllers/link.rs |
HTTP | Two handlers: index (page) and store (create) |
src/routes.rs |
Router | Wires URLs to handlers via routes! |
src/controllers/mod.rs |
Wiring | Re-exports the new controller module |
frontend/src/pages/Links/Index.svelte |
Frontend | The page Inertia renders |
(existing) bootstrap.rs |
Boot | Where you'd register observers/services for this feature |
(existing) .env |
Config | DB URL, ports, secrets |
That's the standard rhythm: migration → model → controller → route → frontend page. Every feature, no matter how big, decomposes into those steps.
What to read next
You've done a full vertical slice. The next things you'll reach for:
- Routing - grouping, middleware, named routes, signed URLs, resource routing
- Validation - what
#[derive(Validate)]gives you - Eloquent - relationships, scopes, observers, soft deletes, the full query builder surface
- Inertia + Frontend - partial reloads, typed props, TypeScript type generation
- Authentication - the auth scaffolding the starter shipped
- Console -
cargo run --bin console <subcommand>and writing your own commands
Or browse documentation.md for the full TOC.
