Quickstart: Build A Blog
This guide uses the current shared-first Netpress structure: one model directory, one migration directory, one seeder directory, and the smart-response router.
Before You Start
Make sure the starter already boots:
npx create-netpress-app my-app
cd my-app
cp .env.example .env
npm run artisan -- key:generate
npm run setup:rendering
npm run migrate
npm run seed
1. Scaffold The Files
npm run artisan -- make:migration create_posts_table
npm run artisan -- make:model Post
npm run artisan -- make:controller PostController --resource
npm run artisan -- make:request StorePostRequest
npm run artisan -- make:seeder PostSeeder
The generated files now land in the shared locations:
database/migrations/app/Models/database/seeders/
2. Create The Posts Table
Update the generated migration:
// database/migrations/<timestamp>_create_posts_table.js
export async function up(database) {
const schema = database.connection("mysql");
await schema.createTable("posts", (table) => {
table.primaryKey("id");
table.foreignId("userId")
.notNullable()
.references("id")
.inTable("users")
.onDelete("cascade");
table.string("title", 255).notNullable();
table.string("slug", 255).notNullable().unique();
table.text("body").notNullable();
table.string("status", 20).notNullable().defaultTo("draft");
table.datetime("publishedAt").nullable();
table.timestamps(true, true);
table.index("userId");
});
}
export async function down(database) {
const schema = database.connection("mysql");
await schema.dropTableIfExists("posts");
}
Replace "mysql" with the connection name your app should target if different. table.primaryKey("id") keeps the migration portable: SQL uses bigIncrements, while MongoDB uses a native ObjectId unless you opt into { mongo: "increments" }.
Then run:
npm run artisan -- migrate
3. Write The Model
Keep the generated static connection = ... line and add your model behavior:
// app/Models/Post.js
import { BaseModel } from "@admicaa/netpress";
import User from "./User.js";
export default class Post extends BaseModel {
static connection = "mysql";
static fillable = ["userId", "title", "slug", "body", "status", "publishedAt"];
static casts = {
publishedAt: "date",
createdAt: "date",
updatedAt: "date",
};
author() {
return this.belongsTo(User, "userId");
}
}
Post.on("creating", (attrs) => {
if (!attrs.slug && attrs.title) {
attrs.slug = attrs.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
});
Add the reverse relation in app/Models/User.js:
import Post from "./Post.js";
posts() {
return this.hasMany(Post);
}
4. Validate Input
// app/Http/Requests/StorePostRequest.js
import { body } from "express-validator";
import { BaseRequest } from "@admicaa/netpress";
export default class StorePostRequest extends BaseRequest {
static rules() {
return [
body("title").trim().notEmpty().isLength({ max: 255 }),
body("body").trim().notEmpty(),
body("status").optional().isIn(["draft", "published"]),
];
}
}
5. Add The Controller
// app/Http/Controllers/PostController.js
import { BaseController, json } from "@admicaa/netpress";
import Post from "../../Models/Post.js";
import StorePostRequest from "../Requests/StorePostRequest.js";
class PostController extends BaseController {
async index(req) {
const page = Number(req.query.page || 1);
return Post.with("author")
.where({ status: "published" })
.latest()
.paginate(page, 15);
}
async show(req) {
return Post.with("author").findOrFail(req.params.id);
}
async store(req) {
const payload = await this.validate(req, StorePostRequest);
const post = await Post.create({ ...payload, userId: req.user.id });
return json(post, 201);
}
}
export default new PostController();
The router will automatically serialize the paginator result from index() as JSON.
6. Add The Routes
Create routes/posts.js:
import { Router } from "@admicaa/netpress";
import PostController from "../app/Http/Controllers/PostController.js";
import authMiddleware from "../app/Http/Middleware/auth.js";
const router = Router();
router.get("/", PostController.index);
router.get("/:id", PostController.show);
router.post("/", authMiddleware, PostController.store);
export default router;
Mount it from routes/api.js:
import postRoutes from "./posts.js";
router.group("/posts", (posts) => {
posts.use("/", postRoutes);
});
Because RouteServiceProvider mounts routes/api.js under /api/v1, the endpoints will live under /api/v1/posts.
7. Seed Example Posts
Create or update database/seeders/PostSeeder.js:
export async function run(db) {
const Post = db.model("Post");
if (!Post) {
throw new Error("Post model is not registered.");
}
const existing = await Post.first({ title: "Hello Netpress" });
if (!existing) {
await Post.create({
userId: 1,
title: "Hello Netpress",
body: "My first post in Netpress.",
status: "published",
publishedAt: new Date(),
});
}
}
Here db.model("Post") resolves the normal Post model class for the seeder context, so Post.create() is still the standard model API. Outside seeders, you would usually import Post directly.
Then call it from database/seeders/DatabaseSeeder.js:
export async function run(db) {
await db.call([
"UserSeeder",
"PostSeeder",
]);
}
Run:
npm run artisan -- db:seed
8. Try The API
curl http://127.0.0.1:3000/api/v1/posts
curl http://127.0.0.1:3000/api/v1/posts/1
After that, you can extend the feature with:
- Relationships for comments and tags
- Controllers for richer response patterns
- Seeders for larger local datasets