Build a Full Stack CRUD App with SolidJS

Master the art of building high-performance full-stack applications using SolidJS, Hono, and PostgreSQL. A comprehensive guide with practical code examples.

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.

Framework Performance Comparison
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: ~34KB

Project Architecture Overview

A well-structured full-stack CRUD application consists of three primary layers working together:

  1. Database Layer: Stores and retrieves data using PostgreSQL or Supabase
  2. API Layer: Exposes endpoints for CRUD operations with Hono or Express.js
  3. 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
);
Database Connection Setup
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).

Complete CRUD API Implementation
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.

SolidJS Reactivity Fundamentals
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.

Todo List Component
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.

Todo Form Component
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

Key Technologies

SolidJS

Fine-grained reactivity for high-performance UIs

Hono

Ultrafast web framework for APIs

PostgreSQL

Robust relational database

Supabase

Backend-as-a-service alternative

Frequently Asked Questions

Ready to Build Your Next Web Application?

Our team of experienced developers specializes in building high-performance web applications using modern technologies like SolidJS.