$ ErwinMVC

View on GitHub →

A lightweight, full-featured MVC framework for Node.js 20+ built with TypeScript. ErwinMVC gets you from zero to a working web application in seconds, with sensible defaults and optional features you can add as needed.

quick start

npx @erwininteractive/mvc init myapp

### philosophy

I built ErwinMVC because I wanted something between "figure it out yourself" and "here's 100 dependencies you don't need." It's opinionated where it matters and flexible where it doesn't.

Start simple, just routes and views. Add a database when you need persistence. Add authentication when you need users. The framework grows with your project instead of front-loading complexity.

### getting started

1. Create a new app

npx @erwininteractive/mvc init myapp
cd myapp

Install dependencies and scaffold a basic app structure

2. Start development server

npm run dev

Visit http://localhost:3000

3. Create a new page

Create src/views/about.ejs:

<!doctype html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= title %></h1>
  <p>Welcome to my about page!</p>
</body>
</html>

4. Add a route

Edit src/server.ts:

app.get("/about", (req, res) {
  res.render("about", { title: "About Us" });
});

5. View your page

Visit http://localhost:3000/about

### creating pages

EJS Templates

Create .ejs files in src/views/

Output a variable (escaped)

<h1><%= title %></h1>

Output raw HTML

<%- htmlContent %>

JavaScript logic

<% if (user) { %>
  <p>Welcome, <%= user.name %>!</p>
<% } %>

Loop through items

<ul>
<% items.forEach(item { %>
  <li><%= item.name %></li>
<% }); %>
</ul>

Include another template

<%- include('partials/header') %>

Adding Routes

Simple page

app.get("/contact", (req, res) {
  res.render("contact", { title: "Contact Us" });
});

Handle form submission

app.post("/contact", (req, res) {
  const { name, email, message } = req.body;
  console.log("Message from " + name + ": " + message);
  res.redirect("/contact?sent=true");
});

JSON API endpoint

app.get("/api/users", (req, res) {
  res.json([{ id: 1, name: "John" }]);
});

### resources

Generate a complete resource (model + controller + views) with one command:

npx erwinmvc generate resource Post

This creates:

  • prisma/schema.prisma - Adds the Post model
  • src/controllers/PostController.ts - Full CRUD controller with form handling
  • src/views/posts/index.ejs - List view
  • src/views/posts/show.ejs - Detail view
  • src/views/posts/create.ejs - Create form
  • src/views/posts/edit.ejs - Edit form

Resource Routes

Action HTTP Method URL Description
index GET /posts List all
create GET /posts/create Show create form
store POST /posts Create new
show GET /posts/:id Show one
edit GET /posts/:id/edit Show edit form
update PUT /posts/:id Update
destroy DELETE /posts/:id Delete

Wiring Up Routes

import * as PostController from "./controllers/PostController";

app.get("/posts", PostController.index);
app.get("/posts/create", PostController.create);
app.post("/posts", PostController.store);
app.get("/posts/:id", PostController.show);
app.get("/posts/:id/edit", PostController.edit);
app.put("/posts/:id", PostController.update);
app.delete("/posts/:id", PostController.destroy);

### controllers

Generate just a controller (without model/views):

npx erwinmvc generate controller Product

This creates src/controllers/ProductController.ts with CRUD actions:

Action HTTP Method URL Description
index GET /products List all
show GET /products/:id Show one
store POST /products Create
update PUT /products/:id Update
destroy DELETE /products/:id Delete

Using controllers:

import * as ProductController from "./controllers/ProductController";

app.get("/products", ProductController.index);
app.get("/products/:id", ProductController.show);
app.post("/products", ProductController.store);
app.put("/products/:id", ProductController.update);
app.delete("/products/:id", ProductController.destroy);

### database (optional)

Your app works without a database. Add one when you need it.

Setup

npm run db:setup

Edit .env with your database URL:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

Run migrations

npx prisma migrate dev --name init

Generate models

npx erwinmvc generate model Post

Edit prisma/schema.prisma to add fields:

model Post {
  id Int @id @default(autoincrement())
  title String
  content String?
  published Boolean @default(false)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  @@map("posts")
}
npx prisma migrate dev --name add-post-fields

Use in code

import { getPrismaClient } from "@erwininteractive/mvc";

const prisma = getPrismaClient();

app.get("/posts", async (req, res) {
  const posts = await prisma.post.findMany();
  res.render("posts/index", { posts });
});

### authentication

ErwinMVC supports two authentication methods:

JWT Authentication

Token-based authentication with bcrypt password hashing. Perfect for traditional username/password authentication.

import {
  hashPassword,
  verifyPassword,
  signToken,
  verifyToken,
  authenticate,
} from "@erwininteractive/mvc";

const hash = await hashPassword("secret123");
const isValid = await verifyPassword("secret123", hash);
const token = signToken({ userId: 1, email: "user@example.com" });

app.get("/protected", authenticate, (req, res) {
  res.json({ user: req.user });
});

WebAuthn (Passkeys)

Passwordless authentication with security keys. Users can log in with biometrics or physical security keys without passwords.

npx erwinmvc webauthn

Environment variables:

  • WEBAUTHN_RP_ID - Your domain (e.g., "localhost" or "example.com")
  • WEBAUTHN_RP_NAME - Your app name (e.g., "ErwinMVC App")

Note: WebAuthn requires HTTPS or localhost.

### cli commands

Init Commands

Command Description
npx @erwininteractive/mvc init <dir> Create a new app
npx erwinmvc generate resource <name> Generate model + controller + views
npx erwinmvc generate controller <name> Generate a CRUD controller
npx erwinmvc generate model <name> Generate a database model
npx erwinmvc webauthn Generate WebAuthn authentication

Init Options

Option Description
--skip-install Skip running npm install
--with-database Include Prisma database setup
--with-ci Include GitHub Actions CI workflow

Resource Options

Option Description
--skip-model Skip generating Prisma model
--skip-controller Skip generating controller
--skip-views Skip generating views
--skip-migrate Skip running Prisma migrate
--api-only Generate API-only controller (no views)

### tailwindcss

Modern utility-first CSS framework for rapid UI development:

Installation

Add Tailwind CSS when creating a new app:

npx @erwininteractive/mvc init myapp --with-tailwind

Includes pre-built Tailwind CSS (no build step needed)

Usage

Use Tailwind classes in your EJS templates:

<nav class="bg-white shadow-lg"> <div class="container mx-auto px-4"> <div class="flex justify-between items-center"> <h1 class="text-2xl font-bold">Logo</h1> </div> </div> </nav>

### project structure

myapp/
├── src/
│ ├── server.ts # Main app - add routes here
│ ├── views/ # EJS templates
│ ├── controllers/ # Route handlers (optional)
│ └── middleware/ # Express middleware (optional)
├── public/ # Static files (CSS, JS, images)
├── prisma/ # Database (after db:setup)
│ └── schema.prisma
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json

Static files in public/ are served at the root URL:

public/css/style.css → /css/style.css public/images/logo.png → /images/logo.png

### app commands

Command Description
npm run dev Start development server (auto-reload)
npm run build Build for production
npm start Run production build
npm run db:setup Install database dependencies
npm run db:migrate Run database migrations

### ci/cd (optional)

Add GitHub Actions CI to your project for automated testing:

npx @erwininteractive/mvc init myapp --with-ci

Or add CI to an existing project by creating .github/workflows/test.yml:

name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

See full documentation on GitHub for database tests, secrets, and more.

### environment variables

All optional. Create .env from .env.example:

DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" # For database
REDIS_URL="redis://localhost:6379" # For sessions
JWT_SECRET="your-secret-key" # For auth
SESSION_SECRET="your-session-secret" # For sessions
PORT=3000 # Server port
NODE_ENV=development # Environment

### learn more

ErwinMVC builds on top of established technologies:

### learn more

Full documentation, examples, and detailed instructions are available in the README.