Master Guides
Tutorial: Build a Blog
From empty folder to a working blog — models, API, and UI.
This tutorial builds a small but complete blog: posts with comments, a JSON API, and a Next.js interface. You’ll use generators, migrations, relationships, and the frontend api() helper.
1. Create the app#
terminal
$ master new grayskull-blog$ cd grayskull-blog$ master db migrate # initialize the SQLite database
2. Scaffold the Post resource#
terminal
$ master g scaffold post title:string body:text published:boolean
This creates the Post model, a RESTful postsController, five routes, and a /posts page. Generate the migration and apply it:
terminal
$ master db new AddPosts$ master db migrate
3. Add comments with a relationship#
terminal
$ master g model Comment body:text author:string
Add the association by hand in the two models:
backend/app/models/Post.js
export default class Post {
id(db) { db.integer().primary().auto(); }
title(db) { db.string().notNullable(); }
body(db) { db.text(); }
published(db) { db.boolean().default(false); }
Comments(db) { db.hasMany('Comment'); }
}backend/app/models/Comment.js
export default class Comment {
id(db) { db.integer().primary().auto(); }
body(db) { db.text(); }
author(db) { db.string(); }
Post(db) { db.belongsTo('Post'); } // creates post_id
}terminal
$ master db new AddComments$ master db migrate
4. A custom API action#
Add an endpoint that returns a post with its comments:
backend/app/controllers/postsController.js
import db from '../models/db.js';
export default class PostsController {
constructor(requestObject) { this.requestObject = requestObject; }
// GET /posts — only published, newest first
async index() {
const data = await db.Post
.where((p) => p.published == true)
.orderBy((p) => p.id, 'desc')
.toList();
this.returnJson({ data });
}
// GET /posts/:id — with comments
async show(obj) {
const post = await db.Post
.where((p) => p.id == $$, Number(obj.params.id))
.include('Comments')
.single();
if (!post) return this.returnError(404, 'Not found');
this.returnJson({ data: post });
}
// POST /posts
async create(obj) {
const post = db.Post.new();
Object.assign(post, obj.params.formData || {});
await post.save();
this.returnJson({ data: post });
}
}Register the routes (the scaffold added most; ensure show is mapped):
backend/app/routes.js
router.route('/posts', 'posts#index', 'get');
router.route('/posts/:id', 'posts#show', 'get');
router.route('/posts', 'posts#create', 'post');5. Build the UI#
frontend/app/posts/page.tsx
import Link from 'next/link';
import { api } from '../lib/api';
interface Post { id: number; title: string }
export default async function PostsPage() {
const { data } = await api<{ data: Post[] }>('/posts');
return (
<main style={{ padding: '2rem' }}>
<h1>Grayskull Blog</h1>
<ul>
{data.map((p) => (
<li key={p.id}><Link href={`/posts/${p.id}`}>{p.title}</Link></li>
))}
</ul>
</main>
);
}6. Run it#
terminal
$ master dev
Create a post and watch it appear:
bash
curl -X POST http://localhost:3001/posts \
-H 'Content-Type: application/json' \
-d '{"title":"I Have the Power","body":"...","published":true}' You built a full-stack app
Models, migrations, a relational API, and a React UI — wired together, no glue code. From here, add auth with an action filter, real-time updates with sockets, and ship it via deployment.