Introduction
SolidStart is a meta-framework based on SolidJS and Solid router (Like Next.js for React or Nuxt for VueJS).
It is still in Beta but we can create great applications already.
In this article, we will see how to integrate a MongoDB database with SolidStart.
To illustrate the method, we will create a basic application: a "Shopping list" webapp.
We assume you have a running MongoDB instance accessible on port 27017.
Preparation
Before integrating MongoDB, we need to prepare our application.
Generate the application
First, we generate a SolidStart application. We use the recommended starter:
mkdir shopping-list && cd shopping-list
npm init solid@latest
And select the bare
template:
? Which template do you want to use?
> bare
hackernews
todomvc
with-auth
with-authjs
with-mdx
with-prisma
with-solid-styled
with-tailwindcss
with-vitest
In the rest of the template generator, we select the usage of Server Side Rendering
and TypeScript
.
Finally, we install NPM dependencies and run dev script:
npm install
npm run dev -- --open
At this step, our application is ready to be developed!
Before adding MongoDB integration, we prepare our application by making some changes.
Model
We create the ShoppingListItem
TypeScript type by creating the file src/models/shopping.ts
with the following content:
src/models/shopping.ts
export type ShoppingListItem = {
_id: string;
label: string;
};
export type NewShoppingListItem = Omit<ShoppingListItem, "_id">;
Style
We add some CSS at the end of src/root.css
.
src/root.css
ul {
display: flex;
flex-direction: column;
align-items: baseline;
}
li {
margin-top: 10px;
}
li > button {
margin-left: 10px;
}
Components
To help us in our app, we create a ListInput
component that lets us type a new item. We create src/components/ListInput.tsx
:
src/components/ListInput.tsx
import { createSignal } from "solid-js";
interface ListInputProps {
onValidate: (value: string) => void;
}
export default function ListInput(props: ListInputProps) {
const [value, setValue] = createSignal("");
const onValidateValue = () => {
props.onValidate(value());
setValue("");
};
return (
<div>
<input
value={value()}
onInput={(evt) => setValue(evt.currentTarget.value)}
type="text"
placeholder="New item"
onKeyPress={(evt) => {
if (evt.key === "Enter") {
evt.preventDefault();
onValidateValue();
}
}}
/>
<button type="button" onClick={onValidateValue}>
Add
</button>
</div>
);
}
Remove the navigation header
The default navigation header from the template is not necessary, so we delete it.
To do this, you just need to remove the <A />
tags in the src/root.tsx
file.
index.tsx page
At this step, we handle the shopping list item with createSignal
. No persistence, but the logic works.
We replace src/routes/index.tsx
with the following content:
src/routes/index.tsx
import { createSignal, For } from "solid-js";
import ListInput from "~/components/ListInput";
import { ShoppingListItem } from "~/models/shopping";
export default function Home() {
const [items, setItems] = createSignal<ShoppingListItem[]>([]);
const addNewItem = (label: string) => {
setItems((prev) => [...prev, { label, _id: Date.now().toString() }]);
};
const deleteItem = (itemId: string) => {
setItems((prev) => prev.filter((item) => item._id !== itemId));
};
return (
<main>
<ul>
<For each={items()}>
{(item) => (
<li>
<span>{item.label}</span>
<button type="button" onClick={() => deleteItem(item._id)}>
Delete
</button>
</li>
)}
</For>
</ul>
<ListInput onValidate={addNewItem} />
</main>
);
}
Add this point, the application works without state persistence.
Integration of MongoDB
Add MongoDB
First, we install mongodb
and @types/mongodb
NPM dependencies:
npm install --save mongodb
npm install --save-dev @types/mongodb
Then, we create a class to manage connection with the database. We create the src/lib/database.ts
with the following content:
src/lib/database.ts
import { Db, MongoClient } from "mongodb";
const MONGODB_HOST = "localhost";
const MONGODB_PORT = 27017;
export class Database {
private db?: Db;
private static instance: Database;
private constructor() {}
async init() {
const client = new MongoClient(`mongodb://${MONGODB_HOST}:${MONGODB_PORT}`);
await client.connect();
this.db = client.db("shopping-list");
}
public static getInstance() {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
getDb() {
if (!this.db) {
throw new Error("DB is not init");
}
return this.db;
}
}
Init the database connection
When our application is launched, we need to launch the database connection.
This is done using the init()
method of the Database
class. The usage of the Singleton pattern helps us.
We edit the src/entry-server.tsx
file to add the database initialization before the createHandler
function:
src/entry-server.tsx
import {
createHandler,
renderAsync,
StartServer,
} from "solid-start/entry-server";
import { Database } from "./lib/database";
// Initialization of the database (connection + retrieve db instance)
const database = Database.getInstance();
database.init();
export default createHandler(
renderAsync((event) => <StartServer event={event} />)
);
Create a ShoppingService to interact with the Database
We create a ShoppingService
to interact with the Database
class and add / list / remove shopping list items.
You can create the file src/lib/shopping-service.ts
with the following content:
src/lib/shopping-service.ts
import { Db, ObjectId } from "mongodb";
import { NewShoppingListItem, ShoppingListItem } from "../models/shopping";
import { Database } from "./database";
export class ShoppingService {
db: Db;
collectionName = "items";
private static instance: ShoppingService;
private constructor() {
this.db = Database.getInstance().getDb();
}
public static getInstance() {
if (!ShoppingService.instance) {
ShoppingService.instance = new ShoppingService();
}
return ShoppingService.instance;
}
async getAllItems(): Promise<ShoppingListItem[]> {
const itemsFromDb = await this.db
.collection(this.collectionName)
.find()
.toArray();
return itemsFromDb.map((elt) => ({
...elt,
_id: elt._id.toString(), // Get _id as String
})) as ShoppingListItem[];
}
async addNewItem(item: NewShoppingListItem) {
await this.db.collection(this.collectionName).insertOne(item);
}
async deleteItem(itemId: string) {
await this.db
.collection(this.collectionName)
.deleteOne({ _id: new ObjectId(itemId) });
}
}
Integration with the front pages
At this point, we need to add the interactions between the ShoppingService
and our index.tsx
page. This will be done in 2 steps:
- Create the
routeData
to retrieve our items and use them via useRouteData - Create the server side actions to add and delete items with createServerAction$
Retrieve our data using routeData
To retrieve data in our page, SolidStart uses routeData
. We create a function named routeData
and use createServerData$
to do actions on the server side and fetch our items.
In our component, we have the useRouteData
function to retrieve our items.
To identify our request (and to be able to invalidate it when we update our data), we create a constant in the src/constants.ts
file.
src/constants.ts
export const SHOPPING_ITEMS_REQ_KEY = "shoppingItems";
Then, we create the function routeData
in our src/routes/index.tsx
file:
src/routes/index.tsx
import { createServerData$ } from "solid-start/server";
import { ShoppingService } from "~/lib/shopping-service";
import { ShoppingListItem } from "~/models/shopping";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants";
export function routeData() {
return createServerData$(
async () => {
const shoppingService = ShoppingService.getInstance();
const data = await shoppingService.getAllItems();
return data as ShoppingListItem[];
},
{ key: SHOPPING_ITEMS_REQ_KEY }
);
}
export default function Home() {
// Content of our page
}
In routeData
, we use ShoppingService
to fetch our items and return them.
To use our retrieved items, we just need to use useRouteData
in our Home
component:
src/routes/index.tsx
import { useRouteData } from "solid-start";
export default function Home() {
const items = useRouteData<typeof routeData>();
// Rest of our component
}
Because we have duplication due to our previous createSignal
logic, we can delete the createSignal
function and edit our addNewItem
and deleteItem
like below:
src/routes/index.tsx
import { useRouteData } from "solid-start";
export default function Home() {
const items = useRouteData<typeof routeData>();
const addNewItem = (label: string) => {};
const deleteItem = (itemId: string) => {};
// Rest of our component
}
For now, our items are retrieved from the database! (But we have no data in the database, we will now add actions to add / delete items to fix this)
Add server side actions
For add and delete actions, we will create some functions that will be executed on the server side. Look at the functions under the routeData
function:
src/routes/index.tsx
import { ShoppingListItem, NewShoppingListItem } from "~/models/shopping";
export function routeData() {
// routeData content
}
async function addNewItemAction(newItem: NewShoppingListItem) {
const shoppingService = ShoppingService.getInstance();
await shoppingService.addNewItem(newItem);
}
async function deleteItemAction(itemId: string) {
const shoppingService = ShoppingService.getInstance();
await shoppingService.deleteItem(itemId);
}
These functions will be called by our Home
component using createServerAction$
that let us call a server side function. It is pecially designed for updating data because this function lets us indicate which data we want to invalidate (to let SolidStart automatically re-fetch them).
We add our actions in our Home
component, under our useRouteData
like this:
src/routes/index.tsx
import { createServerAction$ } from "solid-start/server";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants.ts";
export default function Home() {
const items = useRouteData<typeof routeData>();
const [_adding, addNewItem] = createServerAction$(addNewItemAction, {
invalidate: SHOPPING_ITEMS_REQ_KEY,
});
const [_deleting, deleteItem] = createServerAction$(deleteItemAction, {
invalidate: SHOPPING_ITEMS_REQ_KEY,
});
// Rest of our component
}
We also can delete our empty addNewItem
and deleteItem
functions. They are replaced by our server actions. For more details about the usage of our actions, please refer to the documentation.
In a nutshell, createServerAction$ is returning a tuple containing:
- In first index: The state of the action (pending, error, etc...)
- In the second index: The function that will call our server side action
In our case, we are not using the first index of the tuple.
In options, we also indicate that we invalidate data of the fetch request (using SHOPPING_ITEMS_REQ_KEY
).
Finally, we update the onValidate
prop of ListInput
to match parameters:
src/routes/index.tsx
<ListInput onValidate={(label: string) => addNewItem({ label })} />
And thats it!
Our component looks like this:
src/routes/index.tsx
import { For } from "solid-js";
import { useRouteData } from "solid-start";
import { createServerAction$, createServerData$ } from "solid-start/server";
import ListInput from "~/components/ListInput";
import { ShoppingService } from "~/lib/shopping-service";
import { NewShoppingListItem, ShoppingListItem } from "~/models/shopping";
import { SHOPPING_ITEMS_REQ_KEY } from "~/constants.ts";
export function routeData() {
return createServerData$(
async () => {
const shoppingService = ShoppingService.getInstance();
const data = await shoppingService.getAllItems();
return data as ShoppingListItem[];
},
{ key: SHOPPING_ITEMS_REQ_KEY }
);
}
async function addNewItemAction(newItem: NewShoppingListItem) {
const shoppingService = ShoppingService.getInstance();
await shoppingService.addNewItem(newItem);
}
async function deleteItemAction(itemId: string) {
const shoppingService = ShoppingService.getInstance();
await shoppingService.deleteItem(itemId);
}
export default function Home() {
const items = useRouteData<typeof routeData>();
const [_adding, addNewItem] = createServerAction$(addNewItemAction, {
invalidate: SHOPPING_ITEMS_REQ_KEY,
});
const [_deleting, deleteItem] = createServerAction$(deleteItemAction, {
invalidate: SHOPPING_ITEMS_REQ_KEY,
});
return (
<main>
<ul>
<For each={items()}>
{(item) => (
<li>
<span>{item.label}</span>
<button type="button" onClick={() => deleteItem(item._id)}>
Del
</button>
</li>
)}
</For>
</ul>
<ListInput onValidate={(label: string) => addNewItem({ label })} />
</main>
);
}
Conclusion
Now you know how to integrate MongoDB with SolidStart!
We have seen how to:
- Generate a SolidStart app with the starter
- Use TypeScript with it
- Create a MongoDB connection
- Do server side requests to MongoDB from our components
Feel free to comment to give me your feedback or provide some improvements.
Thanks for reading!
Top comments (0)