In this tutorial, you will learn how to create a user database using nedb
in a ViteJS application. Additionally, we will set up an Express server to handle requests related to users, such as creating, retrieving, updating, and deleting.
Before starting, ensure that you have the following:
First, create a new ViteJS project using the following command:
yarn create vite app-crud
Select the React template when prompted.
Install the necessary dependencies for the project. We will use nedb
for the database and express
for the server:
yarn add nedb express vite-plugin-api-routes react-bootstrap
Organize your project as follows:
/src
/server
/api
/user
GET.js
POST.js
[userId]
GET.js
PUT.js
DELETE.js
/db
index.js
configure.js
/client
/api
dataProvider.js
index.js
/user-admin
index.jsx
main.jsx
index.html
vite.config.js
Execute this comand in the terminal.
# Create base directories
mkdir -p data
mkdir -p src/server/api/user/[userId]
mkdir -p src/server/db
mkdir -p src/client/api
mkdir -p src/client/user-admin
# Create empty files
touch src/server/api/user/GET.js
touch src/server/api/user/POST.js
touch src/server/api/user/[userId]/GET.js
touch src/server/api/user/[userId]/PUT.js
touch src/server/api/user/[userId]/DELETE.js
touch src/server/db/index.js
touch src/server/configure.js
touch src/client/api/dataProvider.js
touch src/client/api/index.js
touch src/client/user-admin/index.jsx
touch src/client/user-admin/table.jsx
touch src/client/user-admin/form.jsx
touch src/client/main.jsx
Create the file src/server/db/index.js
to manage the database using nedb
:
// src/server/db/index.js
import Datastore from "nedb";
export const users = new Datastore({
filename: "./data/users.db",
autoload: true,
});
const db = { users };
export default db;
Set up the Express server in the file src/server/configure.js
:
// src/server/configure.js
import express from "express";
// DEV MODE
export const viteServerBefore = (server, viteServer) => {
console.log("VITEJS SERVER");
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
};
// DEV MODE
export const viteServerAfter = (server, viteServer) => {
const errorHandler = (err, req, res, next) => {
if (err instanceof Error) {
res.writeHead(403, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
} else {
next(err);
}
};
server.use(errorHandler);
};
// PROD MODE
export const serverBefore = (server) => {
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
};
// PROD MODE
export const serverAfter = (server) => {
const errorHandler = (err, req, res, next) => {
if (err instanceof Error) {
res.status(403).json({ error: err.message });
} else {
next(err);
}
};
server.use(errorHandler);
};
Set up the API routes in src/server/api/user
. Below are the necessary routes:
// src/server/api/user/GET.js
import db from "../../db";
const GET_USERS = (req, res, next) => {
db.users.find({}, (err, users) => {
if (err) {
return res.status(500).json({ error: "Error fetching users" });
}
res.status(200).json({ data: users });
});
};
export default GET_USERS;
// src/server/api/user/POST.js
import db from "../../db";
const CREATE_USER = (req, res, next) => {
const { name, email } = req.body;
db.users.insert({ name, email }, (err, user) => {
if (err) {
return res.status(500).json({ error: "Error inserting user" });
}
res.status(201).json({
data: user,
message: "User successfully inserted",
});
});
};
export default CREATE_USER;
// src/server/api/user/[userId]/GET.js
import db from "../../../db";
const GET_USER = (req, res, next) => {
const { userId } = req.params;
db.users.findOne({ _id: userId }, (err, user) => {
if (err) {
return res.status(500).json({ error: "Error fetching user" });
}
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.status(200).json({ data: user });
});
};
export default GET_USER;
// src/server/api/user/[userId]/PUT.js
import db from "../../../db";
const UPDATE_USER = (req, res, next) => {
const { userId } = req.params;
const { name, email } = req.body;
const data = { _id: userId, name, email };
db.users.update(
{ _id: userId },
{ $set: { name, email } },
{},
(err, numReplaced) => {
if (err) {
return res.status(500).json({ data, error: "Error updating user" });
}
if (numReplaced === 0) {
return res.status(404).json({ data, error: "User not found" });
}
res.status(200).json({ data, message: "User successfully updated" });
}
);
};
export default UPDATE_USER;
// src/server/api/user/[userId]/DELETE.js
import db from "../../../db";
const DELETE_USER = (req, res, next) => {
const { userId } = req.params;
db.users.remove({ _id: userId }, {}, (err, numRemoved) => {
if (err) {
return res.status(500).json({ error: "Error deleting user" });
}
if (numRemoved === 0) {
return res.status(404).json({ error: "User not found" });
}
res.status(200).json({ message: "User successfully deleted" });
});
};
export default DELETE_USER;
To configure the vite-plugin-api-routes
plugin, edit the vite.config.js
file:
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import apiRoutes from "vite-plugin-api-routes";
export default defineConfig({
plugins: [
react(),
apiRoutes({
mode: "isolated",
configure: "src/server/configure.js", // Path to the configuration file
dirs: [
{
dir: "src/server/api", // Path to the APIs
route: "",
},
],
}),
],
});
Make sure your index.html
file contains the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CRUD Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client/main.jsx"></script>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
</body>
</html>
Create the src/client/main.jsx
file:
import { StrictMode, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { UserAdmin } from "./user-admin";
const Timer = () => {
const [time, setTime] = useState("");
useEffect(() => {
const update = () => {
const d = new Date();
setTime(d.toISOString());
};
const id = setInterval(update, 1000);
return () => {
clearInterval(id);
};
}, []);
return <h4>{time}</h4>;
};
createRoot(document.getElementById("root")).render(
<StrictMode>
<Timer />
<UserAdmin />
</StrictMode>
);
Create the src/client/api/dataProvider.js
file:
const NOP = (data) => data;
const getQuery = (params = {}) => {
return Object.entries(params)
.reduce((acc, [name, value]) => {
acc.push(`${name}=${value}`);
return acc;
}, [])
.join("&");
};
export const createDataProvider = ({
url = "/api",
resource = "",
assertHeaders = NOP,
assertOptions = NOP,
} = {}) => {
const getList = async (params = {}) => {
const query = getQuery(params);
const headers = assertHeaders({
Accept: "application/json",
});
const options = assertOptions({ headers });
try {
const response = await fetch(`${url}/${resource}?${query}`, options);
const { data, error = "", message = "" } = await response.json();
return { data, error, message };
} catch (error) {
return { error: error.message || "Error fetching the list" };
}
};
const getOne = async (id, params = {}) => {
const query = getQuery(params);
const headers = assertHeaders({
Accept: "application/json",
});
const options = assertOptions({ headers });
try {
const response = await fetch(
`${url}/${resource}/${id}?${query}`,
options
);
const { data, error = "", message = "" } = await response.json();
return { data, error, message };
} catch (error) {
return { error: error.message || "Error fetching the resource" };
}
};
const createOne = async (data) => {
const headers = assertHeaders({
Accept: "application/json",
"Content-Type": "application/json",
});
const options = assertOptions({
headers,
method: "POST",
body: JSON.stringify(data),
});
try {
const response = await fetch(`${url}/${resource}`, options);
const { data, error = "", message = "" } = await response.json();
return { data, error, message };
} catch (error) {
return { error: error.message || "Error creating the resource" };
}
};
const updateOne = async (id, data) => {
const headers = assertHeaders({
Accept: "application/json",
"Content-Type": "application/json",
});
const options = assertOptions({
headers,
method: "PUT",
body: JSON.stringify(data),
});
try {
const response = await fetch(`${url}/${resource}/${id}`, options);
const { data, error = "", message = "" } = await response.json();
return { data, error, message };
} catch (error) {
return { error: error.message || "Error updating the resource" };
}
};
const deleteOne = async (id) => {
const headers = assertHeaders({
Accept: "application/json",
});
const options = assertOptions({
headers,
method: "DELETE",
});
try {
const response = await fetch(`${url}/${resource}/${id}`, options);
const { data, error = "", message = "" } = await response.json();
return { data, error, message };
} catch (error) {
return { error: error.message || "Error deleting the resource" };
}
};
return { getList, getOne, createOne, updateOne, deleteOne };
};
Create the src/client/api/index.js
file:
import { createDataProvider } from "./dataProvider";
export const userDP = createDataProvider({ resource: "user" });
UserAdmin
ComponentNow, create the component src/client/user-admin/index.jsx
:
import React, { createContext, useContext, useEffect, useState } from "react";
import {
Alert,
Button,
ButtonGroup,
Col,
Container,
Form,
InputGroup,
Row,
Spinner,
Table,
} from "react-bootstrap";
import { userDP } from "../api";
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [selectedKey, setSelectedKey] = useState(null);
const [shouldReload, setShouldReload] = useState(true);
return (
<UserContext.Provider
value=
>
{children}
</UserContext.Provider>
);
};
const useUserContext = () => useContext(UserContext);
const UserTable = () => {
const { shouldReload, setShouldReload, setSelectedKey } = useUserContext();
const [searchQuery, setSearchQuery] = useState("");
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [message, setMessage] = useState(null);
const handleEdit = async (userId) => {
setSelectedKey(userId);
};
const handleDelete = async (userId) => {
const { message, error } = await userDP.deleteOne(userId);
setData([]);
setError(error);
setMessage(message);
setSelectedKey(null);
if (!error) {
handleSearch();
}
};
const handleSearch = async () => {
const { data, message, error } = await userDP.getList({ q: searchQuery });
setData(data);
setError(error);
setMessage(message);
setShouldReload(false);
};
useEffect(() => {
if (shouldReload) {
handleSearch();
}
}, [shouldReload]);
return (
<div className="mb-3">
<InputGroup>
<Form.Control
type="text"
placeholder="Buscar..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Button onClick={handleSearch}>Buscar</Button>
</InputGroup>
{shouldReload && <Spinner animation="border" />}
{error && <Alert variant="danger">{error}</Alert>}
{message && <Alert variant="success">{message}</Alert>}
<Table striped bordered hover>
<thead>
<tr>
<th>Nombre</th>
<th>Email</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{data.map((user) => (
<tr key={user._id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<ButtonGroup>
<Button
variant="warning"
onClick={() => handleEdit(user._id)}
>
Editar
</Button>
<Button
variant="danger"
className="ml-2"
onClick={() => handleDelete(user._id)}
>
Eliminar
</Button>
</ButtonGroup>
</td>
</tr>
))}
</tbody>
</Table>
</div>
);
};
const UserForm = () => {
const { selectedKey, setSelectedKey, setShouldReload } = useUserContext();
const [user, setUser] = useState({
name: "",
email: "",
});
const [error, setError] = useState(null);
const [message, setMessage] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
const { data, error, message } = selectedKey
? await userDP.updateOne(user._id, user)
: await userDP.createOne(user);
setError(error);
setMessage(message);
setUser(data);
setShouldReload(true);
};
const handleReset = async () => {
const {
data = {
name: "",
email: "",
},
message,
error,
} = selectedKey ? await userDP.getOne(selectedKey) : {};
setError(error);
setMessage(message);
setUser(data);
};
const onChangeAttr = (name, value) => {
setUser((prev) => {
return { ...prev, [name]: value };
});
};
const handleNew = async () => {
setError("");
setMessage("");
setUser({
name: "",
email: "",
});
setSelectedKey(null);
};
useEffect(() => {
handleReset();
}, [selectedKey]);
return (
<div>
<Form.Control
type="text"
placeholder="Nombre"
value={user.name}
onChange={(e) => onChangeAttr("name", e.target.value)}
/>
<Form.Control
type="text"
placeholder="Email"
value={user.email}
onChange={(e) => onChangeAttr("email", e.target.value)}
/>
<ButtonGroup>
<Button
variant="primary"
type="submit"
className="mt-2"
onClick={handleSubmit}
>
{user._id ? "Actualizar" : "Crear"}
</Button>
<Button variant="secondary" onClick={handleReset} className="mt-2 ml-2">
Resetear
</Button>
<Button variant="warning" onClick={handleNew} className="mt-2 ml-2">
New
</Button>
</ButtonGroup>
{error && <Alert variant="danger">{error}</Alert>}
{message && <Alert variant="success">{message}</Alert>}
</div>
);
};
export const UserAdmin = () => {
return (
<UserProvider>
<Container>
<h1>Gestión de Usuarios</h1>
<Row>
<Col>
<UserTable />
</Col>
<Col>
<UserForm />
</Col>
</Row>
</Container>
</UserProvider>
);
};
To run your application, follow these steps:
Start the Express Server: You need to run the Express backend server in development mode.
In the root of your project, run the following command:
yarn dev
This will start both the frontend development server (Vite) and the backend Express server, making your API routes available.
Access the Application: Open your browser and go to http://localhost:3000
(or the port displayed in your terminal) to view the user management interface.
Testing the Application:
nedb
database, which is a lightweight database file stored locally.In this tutorial, we built a simple CRUD application for managing users using ViteJS, React, Express, and nedb.
Key Takeaways:
This project can be expanded with more features, such as authentication, form validation, and pagination, to handle larger datasets. It’s a great starting point for building full-stack applications with React and Express.
To enhance the application, consider implementing the following features:
If you encounter any issues, here are some tips:
yarn install
.vite-plugin-api-routes
plugin is correctly configured.If you get an error message related to the database, check the permissions for the users.db
file and make sure that the database file exists in the specified location.