Why SolidJS for CRUD Applications
Traditional frameworks like React re-render components when state changes, even if only a small portion of the UI needs updating. SolidJS takes a fundamentally different approach: components run once to set up reactive connections, and updates occur surgically at the exact DOM nodes that need to change. For CRUD applications--which typically involve frequent state updates and user interactions--SolidJS's fine-grained reactivity provides a compelling advantage.
For CRUD applications that often display lists, forms, and detail views with frequent user interactions, this means snappier performance without manual optimization like React's useMemo or useCallback hooks. If you're looking to build a modern web application that delivers exceptional user experiences, SolidJS offers a compelling alternative to traditional frameworks.
1# Performance comparison (typical values)2# SolidJS baseline (1.0x)3# React: 2-3x slower startup, 3x+ slower updates4# Vue: 1.5-2x slower startup, 2x slower updates5 6# Bundle size comparison7SolidJS: ~7KB (core library)8React: ~42KB (with ReactDOM)9Vue: ~34KBProject Architecture Overview
A well-structured full-stack CRUD application consists of three primary layers working together:
- Database Layer: Stores and retrieves data using PostgreSQL or Supabase
- API Layer: Exposes endpoints for CRUD operations with Hono or Express.js
- Frontend Layer: Provides the user interface using SolidJS components
Technology Stack Options
Option 1: Hono + Bun + PostgreSQL - Maximum performance with type-safe RPC Option 2: Supabase - Backend-as-a-service with built-in authentication and real-time subscriptions
When selecting your technology stack, consider the specific needs of your project. For maximum control and performance, a custom backend with web development services expertise delivers tailored solutions. For faster time-to-market, backend-as-a-service platforms reduce development overhead while maintaining scalability.
Setting Up the Backend with Hono and PostgreSQL
Installing Dependencies
# Create project folder
mkdir solidjs-hono-crud
cd solidjs-hono-crud
# Create separate folders for frontend and backend
mkdir client server
# Navigate to server and initialize
cd server
bun init -y
# Install backend dependencies
bun add hono
bun add postgres
bun add @hono/node-server
Database Schema Design
CREATE DATABASE todo_app;
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
1// server/db.ts2import postgres from 'postgres';3 4// Database connection configuration5const sql = postgres({6 host: 'localhost',7 port: 5432,8 database: 'todo_app',9 username: 'your_username',10 password: 'your_password',11});12 13export default sql;Implementing CRUD API Routes
Hono provides an elegant API for defining routes with automatic TypeScript inference. Implement the four CRUD operations: Create (POST), Read (GET), Update (PUT), and Delete (DELETE).
1// server/index.ts2import { Hono } from 'hono';3import { cors } from 'hono/cors';4import sql from './db';5 6// Define the Todo type7type Todo = {8 id: number;9 title: string;10 description: string | null;11 completed: boolean;12 created_at: Date;13};14 15// Create Hono app16const app = new Hono();17app.use('/*', cors());18 19// API routes20const api = new Hono()21 // GET all todos22 .get('/todos', async (c) => {23 const todos = await sql<Todo[]>`SELECT * FROM todos ORDER BY created_at DESC`;24 return c.json(todos);25 })26 27 // GET single todo28 .get('/todos/:id', async (c) => {29 const id = parseInt(c.req.param('id'));30 const todos = await sql<Todo[]>`SELECT * FROM todos WHERE id = ${id}`;31 if (todos.length === 0) {32 return c.json({ error: 'Todo not found' }, 404);33 }34 return c.json(todos[0]);35 })36 37 // POST create todo38 .post('/todos', async (c) => {39 const body = await c.req.json();40 const newTodos = await sql<Todo[]>`41 INSERT INTO todos (title, description) VALUES (${body.title}, ${body.description})42 RETURNING *43 `;44 return c.json(newTodos[0], 201);45 })46 47 // PUT update todo48 .put('/todos/:id', async (c) => {49 const id = parseInt(c.req.param('id'));50 const body = await c.req.json();51 const existing = await sql<Todo[]>`SELECT * FROM todos WHERE id = ${id}`;52 if (existing.length === 0) {53 return c.json({ error: 'Todo not found' }, 404);54 }55 const updated = await sql<Todo[]>`56 UPDATE todos SET 57 title = ${body.title ?? existing[0].title},58 description = ${body.description ?? existing[0].description},59 completed = ${body.completed ?? existing[0].completed}60 WHERE id = ${id} RETURNING *61 `;62 return c.json(updated[0]);63 })64 65 // DELETE todo66 .delete('/todos/:id', async (c) => {67 const id = parseInt(c.req.param('id'));68 const existing = await sql<Todo[]>`SELECT * FROM todos WHERE id = ${id}`;69 if (existing.length === 0) {70 return c.json({ error: 'Todo not found' }, 404);71 }72 await sql`DELETE FROM todos WHERE id = ${id}`;73 return c.json({ success: true });74 });75 76app.route('/api', api);77export default app;Setting Up the SolidJS Frontend
Initializing SolidJS Project
Create a SolidJS project using the official template with degit for quick scaffolding.
# From the project root
npx degit solidjs/templates/js client
cd client
npm install
Core SolidJS Concepts
SolidJS's reactivity system uses signals as the foundation--special objects that notify dependent code when their values change. Understanding signals is essential before building CRUD components, as they form the backbone of SolidJS's fine-grained reactivity model.
1import { createSignal, createMemo, createEffect } from 'solid-js';2 3// Creating a signal4const [count, setCount] = createSignal(0);5 6// Reading the value (note: function call syntax)7console.log(count()); // 08 9// Updating the value10setCount(count() + 1);11 12// Creating a computed value (memo)13const double = createMemo(() => count() * 2);14 15// Effect that runs when dependencies change16createEffect(() => {17 console.log(`Count changed to: ${count()}`);18});Building CRUD Components
Todo List Component
Display all todos with interactive controls for marking completion, editing, and deleting.
1// client/src/components/TodoList.tsx2import { createResource, For, Show } from 'solid-js';3 4interface Todo {5 id: number;6 title: string;7 description: string | null;8 completed: boolean;9}10 11const fetchTodos = async (): Promise<Todo[]> => {12 const response = await fetch('http://localhost:3000/api/todos');13 return response.json();14};15 16export default function TodoList() {17 const [todos, { refetch }] = createResource(fetchTodos);18 19 const toggleComplete = async (id: number, current: boolean) => {20 await fetch(`http://localhost:3000/api/todos/${id}`, {21 method: 'PUT',22 headers: { 'Content-Type': 'application/json' },23 body: JSON.stringify({ completed: !current }),24 });25 refetch();26 };27 28 const deleteTodo = async (id: number) => {29 await fetch(`http://localhost:3000/api/todos/${id}`, { method: 'DELETE' });30 refetch();31 };32 33 return (34 <div class="todo-list">35 <Show when={!todos.loading} fallback={<p>Loading...</p>}>36 <ul class="todo-items">37 <For each={todos()}>38 {(todo) => (39 <li classList={{ completed: todo.completed }}>40 <input41 type="checkbox"42 checked={todo.completed}43 onChange={() => toggleComplete(todo.id, todo.completed)}44 />45 <span>{todo.title}</span>46 <button onClick={() => deleteTodo(todo.id)}>Delete</button>47 </li>48 )}49 </For>50 </ul>51 </Show>52 </div>53 );54}Create Todo Form
Implement a form for adding new todos with proper validation and error handling. Robust form handling is critical for production CRUD applications, ensuring data integrity while providing clear feedback to users.
1// client/src/components/TodoForm.tsx2import { createSignal, Show } from 'solid-js';3 4interface TodoFormProps {5 onTodoCreated: () => void;6}7 8export default function TodoForm(props: TodoFormProps) {9 const [title, setTitle] = createSignal('');10 const [description, setDescription] = createSignal('');11 const [error, setError] = createSignal('');12 const [isSubmitting, setIsSubmitting] = createSignal(false);13 14 const handleSubmit = async (e: Event) => {15 e.preventDefault();16 setError('');17 setIsSubmitting(true);18 19 try {20 const response = await fetch('http://localhost:3000/api/todos', {21 method: 'POST',22 headers: { 'Content-Type': 'application/json' },23 body: JSON.stringify({ title: title(), description: description() || null }),24 });25 26 if (!response.ok) throw new Error('Failed to create todo');27 28 setTitle('');29 setDescription('');30 props.onTodoCreated();31 } catch (err) {32 setError(err instanceof Error ? err.message : 'An error occurred');33 } finally {34 setIsSubmitting(false);35 }36 };37 38 return (39 <form onSubmit={handleSubmit} class="todo-form">40 <Show when={error()}>41 <div class="error-message">{error()}</div>42 </Show>43 44 <div class="form-group">45 <label for="title">Title</label>46 <input47 id="title"48 type="text"49 value={title()}50 onInput={(e) => setTitle(e.currentTarget.value)}51 required52 disabled={isSubmitting()}53 />54 </div>55 56 <div class="form-group">57 <label for="description">Description (optional)</label>58 <textarea59 id="description"60 value={description()}61 onInput={(e) => setDescription(e.currentTarget.value)}62 disabled={isSubmitting()}63 />64 </div>65 66 <button type="submit" disabled={isSubmitting() || !title()}>67 {isSubmitting() ? 'Creating...' : 'Create Todo'}68 </button>69 </form>70 );71}Integrating Supabase as Backend
Supabase provides a compelling alternative for developers who want to skip backend setup while maintaining full CRUD capabilities. The Supabase JavaScript client integrates seamlessly with SolidJS.
Supabase Client Setup
npm install @supabase/supabase-js
// client/src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Database Operations
Supabase provides a fluent API for CRUD operations:
// Create
const { data: newTodo } = await supabase
.from('todos')
.insert([{ title, description }])
.select()
.single();
// Read
const { data: todos } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false });
// Update
const { data: updated } = await supabase
.from('todos')
.update({ completed: true })
.eq('id', id)
.select()
.single();
// Delete
await supabase.from('todos').delete().eq('id', id);
Best Practices and Performance Optimization
Component Optimization
SolidJS inherently optimizes updates through fine-grained reactivity. Use the <For> component instead of array mapping for lists, as it only re-renders items that change.
Optimistic Updates
Implement optimistic updates for better user experience:
async function createTodoOptimistic(todo: { title: string; description: string }) {
// Optimistically add to local store
const optimisticTodo = { id: Date.now(), ...todo, completed: false };
setState('todos', (todos) => [optimisticTodo, ...todos]);
try {
const response = await fetch('http://localhost:3000/api/todos', {
method: 'POST',
body: JSON.stringify(todo),
});
const realTodo = await response.json();
// Replace optimistic todo with real one
setState('todos', (todos) =>
todos.map(t => t.id === optimisticTodo.id ? realTodo : t)
);
} catch (error) {
// Rollback on error
setState('todos', (todos) => todos.filter(t => t.id !== optimisticTodo.id));
}
}
Bundle Size Considerations
SolidJS's small bundle size (~7KB core) is a key advantage. Use dynamic imports for features that aren't immediately needed:
import { lazy } from 'solid-js';
const TodoModal = lazy(() => import('./components/TodoModal'));
Building performant CRUD applications requires attention to both frontend and backend optimization. Our web development team specializes in building high-performance applications using modern frameworks and best practices.
Conclusion
Building a full-stack CRUD application with SolidJS combines the framework's exceptional performance with modern backend technologies to create responsive, maintainable web applications. Whether you choose Hono with PostgreSQL for maximum control or Supabase for rapid development, SolidJS's reactive foundation provides an excellent user experience while keeping bundle sizes small and codebases clean.
Related Resources
- Explore CSS Fundamentals for styling your CRUD interfaces
- Learn about SDK Vs API to understand backend integration patterns
- Review Build File Upload Service Vanilla JavaScript for handling file uploads in CRUD apps
- Discover how AI automation can enhance your application's capabilities with intelligent features
SolidJS
Fine-grained reactivity for high-performance UIs
Hono
Ultrafast web framework for APIs
PostgreSQL
Robust relational database
Supabase
Backend-as-a-service alternative