Netpress Laravel-inspired backend framework for Node.js
Frameworkv0.1.14 Starterv0.1.12 Docsv1.0.3
Overview Installation Architecture CLI
Examples

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: