$ ErwinMVC
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
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
### 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.