TL;DR: This example shows how to build a full-stack application using React Hook Form + RushDB with zero schema setup, backend efforts or data normalization. View the complete source code.
In modern web development, we often spend more time dealing with data modeling, normalization, and database migrations than on the actual business logic that drives our applications. What if we could skip all of that and focus solely on the shape of our data and the user experience?
This article explores how combining React Hook Form with RushDB creates a powerful, backendless approach to fullstack development that eliminates the traditional complexity of working with form data.
The Traditional Form Submission Flow
Let's consider what typically happens when building a form in a web application:
- Define database schema/migrations
- Create models with proper validation
- Build controllers to handle data transformation
- Set up services to process business logic
- Design a REST API to connect your frontend
- Implement API calls from your frontend form
- Normalize nested data before storing it
This approach requires maintaining multiple data representations across your stack, writing repetitive boilerplate code, and continuously synchronizing changes between layers. Even with ORMs and other tools, the complexity remains significant.
The Backendless Approach with RushDB
RushDB offers a radically different approach by providing a graph database that:
- Accepts any JSON structure directly
- Automatically normalizes nested objects into a graph
- Requires zero schema setup or migrations
- Preserves the exact shape of your data
This means you can take your form data exactly as it is structured in your frontend and store it directly without any transformation or normalization.
Let's See It In Action
Let's examine a comprehensive form implementation using React Hook Form with Zod validation and RushDB for storage.
1. Setting Up RushDB
First, we initialize the RushDB client:
// db.ts
import { RushDB } from "@rushdb/javascript-sdk";
// Initialize RushDB
export const db = new RushDB(env.RUSHDB_API_TOKEN);
That's it! No schema definitions, no models, no complex setup.
You can get a free API token by signing up at app.rushdb.com.
For more information on getting started, see the official documentation.
2. Defining Our Form Schema
We use Zod to define the validation schema for our form:
// Form validation schema
const formSchema = z.object({
// Personal Information
firstName: z.string().min(2, "First name must be at least 2 characters"),
lastName: z.string().min(2, "Last name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
phone: z.string().min(10, "Phone number must be at least 10 digits"),
dateOfBirth: z.string().min(1, "Date of birth is required"),
// Address Information (nested object)
address: z.object({
street: z.string().min(5, "Street address is required"),
city: z.string().min(2, "City is required"),
state: z.string().min(2, "State is required"),
zipCode: z.string().min(5, "ZIP code must be at least 5 characters"),
country: z.string().min(2, "Country is required"),
}),
// Professional Information
company: z.string().min(2, "Company name is required"),
position: z.string().min(2, "Position is required"),
experience: z.enum(["entry", "junior", "mid", "senior", "lead", "executive"]),
salary: z.number().min(0, "Salary must be a positive number"),
skills: z.array(z.string()).min(1, "At least one skill is required"),
// Preferences (nested object with boolean values)
preferences: z.object({
newsletter: z.boolean(),
notifications: z.boolean(),
publicProfile: z.boolean(),
dataSharing: z.boolean(),
}),
// Additional Information
bio: z.string().max(500, "Bio must be less than 500 characters"),
website: z.string().url("Invalid URL").optional().or(z.literal("")),
linkedIn: z.string().url("Invalid LinkedIn URL").optional().or(z.literal("")),
github: z.string().url("Invalid GitHub URL").optional().or(z.literal("")),
});
type FormData = z.infer<typeof formSchema>;
Notice how we naturally represent the form data with nested objects and arrays - this matches exactly how we think about the data in our application.
3. Submitting Form Data to RushDB
The most remarkable part is how we handle form submission:
const onSubmit = async (data: FormData) => {
try {
// Save to RushDB
await db.records.createMany({
label: "FORM_DATA",
data,
});
reset(); // Reset form after successful submission
} catch (error) {
// ..
} finally {
// ..
}
};
That's it! Just a single API call to store the entire form data, including nested objects and arrays, without any normalization or transformation.
This image shows the actual network request containing the raw form data that gets sent to RushDB. Notice how the nested structure stays intact with no transformation required.
What's Happening Behind the Scenes?
When you submit the form data to RushDB:
- The entire JSON structure is accepted as-is
- Nested objects (like
address
andpreferences
) are automatically normalized into separate nodes in the graph - Array items (like
skills
) are stored as a normal Property ofFORM_DATA
Record - The original structure is preserved for querying later
RushDB handles all the complexity of storing a deeply nested object structure while maintaining the relationships between entities.
The 3D graph visualization shows how RushDB has automatically normalized your form data into a graph structure. Each node represents an entity from your form, and the edges show the relationships between them.
Despite being stored as a graph, RushDB also provides this familiar table view of your data. This makes it easy to query and analyze your data in a familiar way.
Benefits of This Approach
1. Zero Backend Complexity
No need to:
- Define schemas or migrations
- Create models or DTOs
- Build controllers and services
- Normalize data manually
2. Preserve Natural Data Structures
- Store data in the same shape as your application thinks about it
- No need to flatten nested objects
- Arrays are handled seamlessly
3. Full-Stack Type Safety
- Your Zod schema generates TypeScript types that can be used throughout your application
- The same types work for both form validation and data storage
4. Rapid Development
- Focus on business logic and UX, not database structure
- Iterate quickly without worrying about schema changes
- Add new fields without migrations
Querying the Data
One of the most powerful aspects of RushDB is its ability to query this complex data structure easily. For example, finding all form submissions from senior engineers:
const seniorEngineers = await db.records.find({
labels: ["FORM_DATA"],
where: {
experience: "senior",
position: { $contains: "Engineer" },
},
});
Or finding all users who have subscribed to the newsletter and have JavaScript skills:
const subscribedJsDevelopers = await db.records.find({
labels: ["FORM_DATA"],
where: {
preferences: {
newsletter: true,
},
skills: { $contains: "JavaScript" },
},
});
Conclusion
The combination of React Hook Form for frontend validation and RushDB for data storage creates a powerful backendless approach to full-stack development. It allows you to focus on what really matters - the shape of your data and the user experience - without getting bogged down in database schemas, normalization, and complex backend architectures.
This approach is particularly well-suited for:
- Rapid prototyping and MVP development
- Applications with complex, nested data structures
- Teams looking to reduce backend complexity
- Projects where the data model is evolving rapidly
By removing the traditional backend complexity, you can build sophisticated applications faster and with less code. RushDB's ability to handle nested JSON structures directly means you can model your data in the most natural way for your application without worrying about how it will be stored.
For more information about RushDB's capabilities, check out the official documentation and explore the examples repository.
Source Code: The complete example from this article is available at github.com/rush-db/examples/tree/main/vite-react-useForm