A lightweight, full-featured MVC framework for Node.js with Express, Prisma, and EJS
npm install @erwininteractive/mvcA lightweight, full-featured MVC framework for Node.js 20+ built with TypeScript.
- Express - Fast, minimal web framework
- EJS + Alpine.js - Server-side templating with reactive client-side components
- Optional Database - Add Prisma + PostgreSQL when you need it
- Optional Redis Sessions - Scalable session management
- JWT Authentication - Secure token-based auth with bcrypt password hashing
- CLI Tools - Scaffold apps and generate models/controllers
``bash`
npx @erwininteractive/mvc init myapp
cd myapp
npm run dev
Visit http://localhost:3000 - your app is running!
---
Create src/views/about.ejs:
` Welcome to my about page!html`
<%= title %>
Edit src/server.ts:
`typescript`
app.get("/about", (req, res) => {
res.render("about", { title: "About Us" });
});
Visit http://localhost:3000/about
---
Create .ejs files in src/views/. EJS lets you use JavaScript in HTML:
`html<%= title %>
<%- htmlContent %>
<% if (user) { %>
Welcome, <%= user.name %>!
<%- include('partials/header') %>
`
`typescript
// 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" }]);
});
`
---
Generate a complete resource (model + controller + views) with one command:
`bash`
npx erwinmvc generate resource Post
This creates:
- prisma/schema.prisma - Adds the Post modelsrc/controllers/PostController.ts
- - Full CRUD controller with form handlingsrc/views/posts/index.ejs
- - List viewsrc/views/posts/show.ejs
- - Detail viewsrc/views/posts/create.ejs
- - Create formsrc/views/posts/edit.ejs
- - Edit form
| 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 |
Add to src/server.ts:
`typescript
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);
`
---
Generate just a controller (without model/views):
`bash`
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 |
`typescript
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);
`
---
Your app works without a database. Add one when you need it.
`bash`
npm run db:setup
Edit .env with your database URL:
``
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
Run migrations:
`bash`
npx prisma migrate dev --name init
`bash`
npx erwinmvc generate model Post
Edit prisma/schema.prisma to add fields:
`prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posts")
}
`
Run migrations again:
`bash`
npx prisma migrate dev --name add-post-fields
`typescript
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 });
});
`
---
`typescript
import {
hashPassword,
verifyPassword,
signToken,
verifyToken,
authenticate,
} from "@erwininteractive/mvc";
// Hash a password
const hash = await hashPassword("secret123");
// Verify a password
const isValid = await verifyPassword("secret123", hash);
// Sign a JWT
const token = signToken({ userId: 1, email: "user@example.com" });
// Protect routes with middleware
app.get("/protected", authenticate, (req, res) => {
res.json({ user: req.user });
});
`
---
| Command | Description |
|---------|-------------|
| npx @erwininteractive/mvc init
| Create a new app |
| npx erwinmvc generate resource | Generate model + controller + views |
| npx erwinmvc generate controller | Generate a CRUD controller |
| npx erwinmvc generate model | Generate a database model |$3
| Option | Description |
|--------|-------------|
|
--skip-install | Skip running npm install |
| --with-database | Include Prisma database setup |
| --with-ci | Include GitHub Actions CI workflow |$3
| 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) |$3
| Option | Description |
|--------|-------------|
|
--skip-migrate | Skip running Prisma migrate (model) |
| --no-views | Skip generating EJS views (controller) |---
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
`$3
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:
`bash
npx @erwininteractive/mvc init myapp --with-ci
`Or add CI to an existing project by creating
.github/workflows/test.yml:`yaml
name: Teston:
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
`$3
If your app uses a database, add PostgreSQL as a service:
`yaml
jobs:
test:
runs-on: ubuntu-latest services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
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 migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
- name: Build
run: npm run build
`$3
For production deployments, add these secrets in your GitHub repository settings:
| Secret | Description |
|--------|-------------|
|
DATABASE_URL | Production database connection string |
| REDIS_URL | Production Redis connection string |
| JWT_SECRET | Secret key for JWT signing |
| SESSION_SECRET | Secret key for session encryption |Access secrets in your workflow:
`yaml
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
`---
Environment Variables
All optional. Create
.env from .env.example:`env
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
``---
- Express.js Documentation
- EJS Documentation
- Prisma Documentation
- Alpine.js Documentation
MIT