Migrate from PHP/CodeIgniter to Node.js/Remix while keeping the familiar development experience and gaining modern performance and UX.
- Full page reload on every action
- Slow loading with white screen time
- Long wait time after form submission
- Difficult frontend/backend separation
- Repetitive CRUD code
- Manual SQL writing for every table
- Lack of modern frontend components
- Package management not as convenient as npm
- No type checking
- Dependent on Apache/PHP environment
- Difficult to containerize
- Complex horizontal scaling
- React Hooks, component lifecycle concepts
- Complex TypeScript type system
- Too much boilerplate code
- No Model like CodeIgniter
- Need to write CRUD logic for every table
- Lots of repetitive SQL code
- Multiple files needed to handle one resource
_index.tsx,$id.tsx,edit.$id.tsxscattered- Not as clear as CodeIgniter's Controller
We created a minimalist architecture that solves all disadvantages of both PHP and Remix:
Problem: Both PHP and Remix require manual repetitive CRUD SQL
Solution:
// β Traditional way (both PHP and Remix)
db.prepare("INSERT INTO users (name, email) VALUES (?, ?)").run(name, email);
db.prepare("UPDATE users SET name = ?, email = ? WHERE id = ?").run(name, email, id);
// β
Our way: Auto-generated SQL
const userBase = new BaseModel('users');
userBase.create({ name, email }); // π Auto-generates INSERT
userBase.update(id, { name, email }); // π Auto-generates UPDATE
userBase.delete(id); // π Auto-generates DELETEAdvantages:
- β Zero SQL code
- β Auto-generates correct SQL based on object
- β Supports any field combination
Problem: Remix needs multiple files to handle one resource
Solution:
// β Traditional Remix (needs 2-3 files)
users._index.tsx β /users
users.$id.tsx β /users/:id
users.edit.$id.tsx β /users/edit/:id
// β
Our way: One file
users.($action).($id).tsx β /users, /users/edit/1Code Example:
// app/routes/users.($action).($id).tsx
const userBase = new BaseModel('users');
export async function loader({ params }) {
// GET /users/edit/1
if (params.action === 'edit' && params.id) {
return json({ user: userBase.getById(params.id) });
}
// GET /users
return json({ users: userBase.getAll() });
}
export async function action({ request }) {
const formData = await request.formData();
if (intent === "create") {
userBase.create({
name: formData.get("name"),
email: formData.get("email")
}); // π Auto-generates SQL!
}
}Advantages:
- β One file = Complete CRUD
- β As clear as CodeIgniter Controller
- β Reduces 80% file count
Problem: TypeScript too complex vs PHP has no type checking
Solution: We support both ways, you choose!
// app/models/User.model.ts
// Define interface (optional)
export interface User {
id: number;
name: string;
email: string;
}
// β
Way 1: With types (TypeScript lovers)
export const userBaseTyped = new BaseModel<User>('users');
// β
Way 2: Fully any (PHP developers)
export const userBase = new BaseModel('users');Usage Comparison:
// Way 1: Type hints (editor auto-complete)
const users: User[] = userBaseTyped.getAll();
users[0].name // β
Editor suggests name, email properties
users[0].nmae // β Editor error: property doesn't exist
// Way 2: No types (like PHP, free and flexible)
const users = userBase.getAll();
users[0].name // β
Works, but no auto-complete
users[0].anything // β
No error, runtime only knowsAdvantages:
- β
Free choice: Want type safety? Use
<User>. Want flexibility? Don't. - β Same BaseModel, two ways to use
- β Team can mix and match
| Feature | CodeIgniter | Traditional Remix | Our Solution |
|---|---|---|---|
| CRUD SQL | Manual | Manual | β Auto-generated |
| File Count | 2-3 | 3-5 | β 1 |
| Type System | None | Complex | β Optional (use any) |
| User Experience | Full page reload | Good | β Good |
| Learning Curve | Low | High | β Low |
| Development Speed | Fast | Slow | β Fastest |
// app/models/BaseModel.ts
export class BaseModel<T = any> {
constructor(tableName: string) { }
getAll(): T[]
getById(id: any): T | undefined
// π Auto-generates SQL based on object
create(data: any): T | undefined {
const keys = Object.keys(data);
const values = Object.values(data);
const sql = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${keys.map(() => '?').join(', ')})`;
// Auto-generates: INSERT INTO users (name, email) VALUES (?, ?)
}
update(id: any, data: any): T | undefined {
const keys = Object.keys(data);
const values = Object.values(data);
const sql = `UPDATE ${tableName} SET ${keys.map(k => `${k} = ?`).join(', ')} WHERE id = ?`;
// Auto-generates: UPDATE users SET name = ?, email = ? WHERE id = ?
}
delete(id: any): boolean
}// app/routes/users.($action).($id).tsx
import { BaseModel } from "~/models/BaseModel";
const userBase = new BaseModel('users');
export async function loader({ params }) {
if (params.action === 'edit' && params.id) {
return json({ user: userBase.getById(params.id) });
}
return json({ users: userBase.getAll() });
}
export async function action({ request }) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "create") {
userBase.create({
name: formData.get("name"),
email: formData.get("email")
});
}
if (intent === "update") {
userBase.update(params.id, {
name: formData.get("name"),
email: formData.get("email")
});
}
if (intent === "delete") {
userBase.delete(formData.get("id"));
}
return redirect("/users");
}
export default function Users() {
const { users, user } = useLoaderData();
if (user) {
return <EditView user={user} />;
}
return <IndexView users={users} />;
}npm installnpm run devhttp://localhost:5173/users
Step 1: Create Database Table
// app/config/database.ts
db.exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL
)
`);Step 2: Create Route File
// app/routes/products.($action).($id).tsx
const productBase = new BaseModel('products');
export async function loader({ params }) {
if (params.action === 'edit' && params.id) {
return json({ product: productBase.getById(params.id) });
}
return json({ products: productBase.getAll() });
}
export async function action({ request }) {
const formData = await request.formData();
if (intent === "create") {
productBase.create({
name: formData.get("name"),
price: formData.get("price")
}); // π Auto-generates: INSERT INTO products (name, price) VALUES (?, ?)
}
}Step 3: Done!
That simple!
app/
βββ config/
β βββ database.ts # Database configuration
βββ models/
β βββ BaseModel.ts # Core: Auto-generates SQL
β βββ User.model.ts # Optional: Type definition demo
βββ routes/
β βββ _index.tsx # Redirect to /users
β βββ users.($action).($id).tsx # User CRUD (one file)
βββ views/
βββ users/
βββ IndexView.tsx # List view
βββ EditView.tsx # Edit view
Note: User.model.ts is optional, only for type system demo. You can:
- Skip it: Directly
new BaseModel('users')in routes (like PHP) - Use it: Import
userBaseoruserBaseTyped(with type hints)
No SQL writing needed, BaseModel auto-generates based on passed object
users.($action).($id).tsx matches:
/usersβ List/users/edit/1β Edit
Two ways to use in routes:
// app/routes/users.($action).($id).tsx
// β
Way 1: Direct creation (no types, simplest)
const userBase = new BaseModel('users');
export async function loader({ params }) {
const users = userBase.getAll(); // any[]
return json({ users });
}
// β
Way 2: Import with types (has hints)
import { userBaseTyped, User } from "~/models/User.model";
export async function loader({ params }) {
const users = userBaseTyped.getAll(); // User[]
return json({ users });
}Your Choice:
- π’ Beginners/PHP developers β Way 1 (direct creation, no types)
- π΅ TypeScript lovers β Way 2 (import, with types)
- π£ Team projects β Mix (each takes what they need)
- Simple API
- Fast development
- Low learning curve
- No page reload (smooth page transitions)
- Modern ecosystem (npm packages)
- Containerized deployment (Docker)
- Auto-generate SQL (zero repetitive code)
- React components
- SSR performance
- Type safety (optional)
- One file does it all (no need for multiple route files)
- Auto SQL (no manual CRUD)
- Optional types (no complex TypeScript)
MIT
Thanks to CodeIgniter's simple design philosophy and Remix's modern architecture.
From PHP to Node.js, development experience not compromised, but better! π