SolidStart: Integration with MongoDB
Tags: solidjs, solidstart, mongodb, typescript
Published: 2023-04-04 13:48:14

## 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.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m2pw68vei4kod0whlz7h.png)

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](https://start.solidjs.com/getting-started/project-setup):

```shell
mkdir shopping-list && cd shopping-list
npm init solid@latest
```

And select the `bare` template:

```shell
? 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:

```shell
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**

```typescript
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**

```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**

```typescript
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**

```typescript
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:

```shell
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**

```typescript
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**

```typescript
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**

```typescript
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:

1. Create the `routeData` to retrieve our items and use them via [useRouteData](https://start.solidjs.com/api/useRouteData)
2. Create the server side actions to add and delete items with [createServerAction$](https://start.solidjs.com/api/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**

```typescript
export const SHOPPING_ITEMS_REQ_KEY = "shoppingItems";
```

Then, we create the function `routeData` in our `src/routes/index.tsx` file:

**src/routes/index.tsx**

```typescript
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**

```typescript
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**

```typescript
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**

```typescript
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**

```typescript
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$](https://start.solidjs.com/api/createServerAction) is returning a tuple containing:

1. In first index: The state of the action (pending, error, etc...)
2. 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**

```typescript
<ListInput onValidate={(label: string) => addNewItem({ label })} />
```

And thats it!

Our component looks like this:

**src/routes/index.tsx**

```typescript
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!