Jac vs Traditional Stack: A Side-by-Side Comparison#
This document compares building the same Todo application using Jac versus a traditional Python + FastAPI + SQLite + TypeScript + React stack.
Jac Implementation#
# This single jac program is a fullstack application
node Todo {
has title: str,
done: bool;
}
def:pub get_todos -> list {
root ++> [
Todo("build startup", False),
Todo("raise funding", False),
Todo("change the world", False)
];
return [{"title": t.title, "done": t.done} for t in [root-->](`?Todo)];
}
cl def:pub app() -> any {
has items: list = [];
async can with entry {
items = await get_todos();
}
return
<div>
{[<div key={item.title}>
<input type="checkbox" checked={item.done} />
{item.title}
</div> for item in items]}
</div>;
}
with entry {
}
What this file provides:
node Tododefines the data model with automatic persistence to a graph databasedef:pub get_todoscreates an HTTP API endpointcl def:pub app()defines a React component that runs on the clienthas itemsbecomesuseStatein the generated JavaScriptasync can with entrybecomesuseEffect(() => {...}, [])for loading data on mountwith entryseeds initial data into the graph databaseawait get_todos()handles the HTTP request to the backend
Traditional Stack Implementation#
The equivalent functionality using Python, FastAPI, SQLite, TypeScript, and React.
Backend#
backend/requirements.txt#
Purpose: Lists Python package dependencies. Python's package manager (pip) uses this file to install FastAPI (web framework), Uvicorn (ASGI server), SQLAlchemy (database ORM), and Pydantic (data validation).
backend/database.py#
"""Database configuration and session management."""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
SQLALCHEMY_DATABASE_URL = "sqlite:///./todos.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency to get database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
Purpose: Configures the SQLite database connection, creates the SQLAlchemy engine, sets up session management, and provides a dependency function for database access in API endpoints.
backend/models.py#
"""SQLAlchemy models and Pydantic schemas."""
from sqlalchemy import Column, Integer, String, Boolean
from pydantic import BaseModel
from database import Base
class TodoModel(Base):
"""SQLAlchemy model for Todo items."""
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
done = Column(Boolean, default=False)
class TodoResponse(BaseModel):
"""Pydantic schema for Todo response."""
title: str
done: bool
class Config:
from_attributes = True
Purpose: Defines the data structure twice: once as a SQLAlchemy model for database operations, and once as a Pydantic schema for API response serialization.
backend/main.py#
"""FastAPI Todo Application - Backend API."""
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from database import engine, get_db, Base
from models import TodoModel, TodoResponse
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database on startup."""
Base.metadata.create_all(bind=engine)
db = next(get_db())
if db.query(TodoModel).count() == 0:
initial_todos = [
TodoModel(title="build startup", done=False),
TodoModel(title="raise funding", done=False),
TodoModel(title="change the world", done=False),
]
db.add_all(initial_todos)
db.commit()
db.close()
yield
app = FastAPI(title="Todo API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/todos", response_model=list[TodoResponse])
def get_todos(db: Session = Depends(get_db)):
"""Get all todos."""
return db.query(TodoModel).all()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Purpose: Creates the FastAPI application, configures CORS middleware for frontend access, defines the API endpoint, handles database initialization on startup, and seeds initial data.
Frontend#
frontend/package.json#
{
"name": "todo-app-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}
Purpose: Node.js package manifest that lists JavaScript/TypeScript dependencies (React, TypeScript, Vite) and defines npm scripts for development and building.
frontend/tsconfig.json#
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Purpose: Configures the TypeScript compiler with target JavaScript version, module resolution strategy, JSX handling, and type-checking rules.
frontend/tsconfig.node.json#
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
Purpose: Separate TypeScript configuration for Node.js-executed files like vite.config.ts, which run in a different environment than browser code.
frontend/vite.config.ts#
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
})
Purpose: Configures Vite build tool with React plugin support and sets up a development proxy to forward /api requests to the backend server.
frontend/index.html#
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Purpose: HTML entry point that provides the root DOM element where React mounts and loads the TypeScript entry point.
frontend/src/types.ts#
Purpose: TypeScript interface definitions for data structures. These must be kept in sync with the backend Pydantic schemas.
frontend/src/api.ts#
import { Todo } from './types';
export async function getTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos');
if (!response.ok) {
throw new Error('Failed to fetch todos');
}
return response.json();
}
Purpose: API client function that wraps the fetch call to communicate with the backend, handling HTTP requests and error checking.
frontend/src/App.tsx#
import { useState, useEffect } from 'react';
import { Todo } from './types';
import { getTodos } from './api';
export default function App() {
const [items, setItems] = useState<Todo[]>([]);
useEffect(() => {
async function loadTodos() {
const todos = await getTodos();
setItems(todos);
}
loadTodos();
}, []);
return (
<div>
{items.map((item) => (
<div key={item.title}>
<input type="checkbox" checked={item.done} readOnly />
{item.title}
</div>
))}
</div>
);
}
Purpose: Main React component that manages state with useState, loads data on mount with useEffect, and renders the todo list.
frontend/src/main.tsx#
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Purpose: React application entry point that mounts the App component into the DOM root element.
What Each Approach Requires#
| Component | Traditional Stack | Jac |
|---|---|---|
| Database configuration | Explicit setup | Automatic |
| ORM model | Required | node declaration |
| API serialization schema | Required (Pydantic) | Automatic |
| API route definition | Required (decorator) | def:pub |
| CORS configuration | Required | Automatic |
| Frontend type definitions | Required | Shared with backend |
| API client code | Required | Automatic RPC |
| React state setup | useState hook |
has declaration |
| Data loading effect | useEffect hook |
can with entry |
| Build tooling config | Required (Vite, TS) | Automatic |
| HTML entry point | Required | Automatic |