Using Nuxt-Ionic: Prisma SQLite & Ionic Framework w/ Capacitor
Tags: nuxt, ionic, prisma, webdev
Published: 2022-07-01 14:16:39


![Logos of tech](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3deq0lskj8588vjvvga.png)

This is a follow-up to my video on using [nuxt-ionic](https://ionic.roe.dev/) to build and deploy a mobile application using [Ionic Framework Vue Components](https://ionicframework.com/vue) and [Capacitor](https://capacitorjs.com/) for deploying to native devices. 

> Link to the first video [Intro To Nuxt Ionic](https://youtu.be/f4sB7NhCgRw)

In this post we will add a Camera using Capacitor Camera Plugin, add a backend with using [Prisma](https://www.prisma.io/) with [SQLite](https://www.prisma.io/dataguide/sqlite) as our database, deploy to mobile device and run the server creating a full-stack mobile experience using VueJS [Nuxt 3](https://v3.nuxtjs.org/) and Ionic Framework

## The Video
{% youtube LpJMR-PlzYU %}

## Installing Prisma

```jsx
npm install prisma@latest typescript ts-node @types/node --save-dev
npm install @prisma/client@latest
npx prisma init --datasource-provider sqlite
```

## Server Related Actions

Create the database models for the application. Notice I have specified the database url directly in the file and not used an `.env` file.

```javascript
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./mydata.db"
}

model ImagePost {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   
  content   String?
  image     String?
  published Boolean  @default(false)
}
```

Command to migrate changes to database

```shell
npx prisma migrate dev --name initialize_db
```
Since we are using Nuxt for our backend we will create two api routes in the `server/api/` directory; one for getting all of ImagePost records and another for adding them.

Prisma client code

```javascript
// server/utils/prisma-client.ts

import prismaClient from '@prisma/client';

const { PrismaClient } = prismaClient;

export default new PrismaClient({});
```

Server API for getting all of the ImagePosts is below. We are using the prisma client we initialized above.

```jsx
// server/api/getAllPosts

import dbClient from '../utils/prisma-client';

export default defineEventHandler(async ({ req }) => {

  const data = await dbClient.imagePost.findMany();
  return  data 
});
```

```jsx
// server/api/addPost

import dbClient from '../utils/prisma-client';

export default defineEventHandler(async (event) => {
    const body = await useBody(event)
    console.log(body);
    
    const resp = await dbClient.imagePost.create({
        data: {
            title: body.title,
            content:  body.content,
            image:  body.image,
        }
    });
    return { resp }
});
```


You will modify your runtime configuration to support the url for the api when you are running locally and developing in the browser which is development; and when testing on device/phone which is production production in this configuration.

```javascript
// nuxt.config.ts

import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      API_URL: process.env.NODE_ENV === "development" ? "/api" : 'http://192.168.1.56:3000/api',
    }
  },
  modules: ['nuxt-ionic'],
  ssr: true,
  meta: [{
    name: "viewport",
    content: "viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
  }
  ],
  ionic: {
    integrations: {
      //
      // pwa: true,
      // router: true,
    },
    css: {
      // basic: false,
      // core: false,
      utilities: true,
    },
  },
})
```

## Client Related Actions

**Code Changes in HomePage.vue**

template

```html
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Nuxt Ionic Prisma Photo Demo</ion-title>
        <ion-buttons slot="end">
          <ion-button>LOGOUT</ion-button>
        </ion-buttons>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">

      <!-- indicators -->
      <ion-loading :is-open="pending" message="LOADING..."></ion-loading>
      <ion-loading :is-open="saving" message="SAVING..."></ion-loading>

			<!-- modal to get title and content before saving -->
      <image-post-input
        :showImagePostInput="showImagePostInput"
        :imageURL="imageURL"
        @image-post-submit="doSave"
        @image-post-cancel="showImagePostInput = false"
      />
      <p>
        Sample app with Nuxt for server and client mobile app. Prisma for saving the data
        to database and Ionic / Capacitor for device capabilities
      </p>

			<!-- click to take photo and save to database -->
      <ion-button @click="doCamera"> CREATE IMAGE POST </ion-button>

      <!-- loop through records in database -->
      <ion-card v-for="item in data" :key="item?.id">
        <ion-card-header>
          <ion-card-title>{{ item?.title }}</ion-card-title>
          <ion-card-subtitle>{{ item?.content }}</ion-card-subtitle>
        </ion-card-header>
        <ion-card-content v-if="item?.image">
          <ion-img :src="(item?.image as any)" />
        </ion-card-content>
      </ion-card>
    </ion-content>

  </ion-page>
</template>
```

imports for the component main HomePage Component

```tsx
import { Camera, CameraResultType, ImageOptions } from "@capacitor/camera";
import { alertController } from "@ionic/vue";
import { ref } from "vue";
```

extra code added to the head of the page to load ionic pwa components, this helps us to use a devices camera when running the app as a PWA

```tsx
useHead({
  script: [
    {
      async: true,
      crossorigin: "anonymous",
      type: "module",
      src:
        "https://unpkg.com/@ionic/pwa-elements@latest/dist/ionicpwaelements/ionicpwaelements.esm.js",
    },
  ],
});
```

local properties used in the component

```tsx
// router
const ionRouter = useIonRouter();

// ref holding the image from the camera
const imageURL = ref<string | null>(null);

// flag for rendering the saving indicator ui element
const saving = ref<boolean>(false);

// flag for rendering the ImagePostInput Modal
const showImagePostInput = ref<boolean>(false);

// api url from config to support dev and prod api calls
const API_URL = useRuntimeConfig().public.API_URL;
```

query the database using the api route we created

```tsx
const { data, pending, error, refresh } = await useAsyncData<ImagePostArray>(
  "posts",
  () => $fetch(`${API_URL}/getAllPosts`)
);
```

We using the `pending` to indicate we are loading, `data` is the result from the query, `error` is used to show an alert if there is a problem and `refresh` is used to reload the data after we add a new ImagePost

Alert code for when there is an error and the function we use for displaying an alert since it appears multiple times in the application

```tsx
const doAlert = (options: { header: string; message: string }) => {
  return alertController
    .create({ buttons: ["OK"], ...options })
    .then((alert) => alert.present());
};

// display error if necessary
if (error?.value) {
  doAlert({
    header: "Error Loading Data",
    message: (error?.value as Error)?.message,
  });
}
```

This is the function to take the picture, it is called when button is clicked in the template. If an image is captured, we set the ref `imageURL` which will render the image in the input form. the last thing we do is set the boolean flag to show the modal component `ImagePostInput`

```tsx
const doCamera = async () => {
  const image = await Camera.getPhoto({
    quality: 90,
    // allowEditing: true,
    correctOrientation: true,
    width: 400,
    resultType: CameraResultType.Base64,
  });

  imageURL.value = `data:${image.format};base64,${image.base64String}`;
  
  // show dialog to confirm image
  showImagePostInput.value = true;
};
```

function for saving the ImagePost to the data base using the server api route we created. it is called if the submit event is emitted from the dialog

```tsx
const doSave = async (
  { title, content }: { title: string; content: string }
) => {
  // hide the input form
  showImagePostInput.value = false;

  // show the saving indicator
  saving.value = true;

  try {
    const dataToSave: Partial<ImagePost> = {
      title,
      content,
      image: imageURL.value,
      published: true,
    };

    await $fetch(`${API_URL}/post`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(dataToSave),
    });
    imageURL.value = null;

    // reload the data for the UI
    await refresh();

    // hide saving ui element
    saving.value = false;

    // display alert to indicate successful save
    doAlert({
      header: "Saving Image Post",
      message: "Image saved successfully",
    });
  } catch (error) {
    saving.value = false;
    doAlert({
      header: "Error",
      message: error.message,
    });
  }
};
```

**ImagePostInput Component - Capture additional Data and Save To Database**

Input form for capturing additional information about the photo, this is trigger to be opened after the user takes a photo with the camera

```html
<template>
  <IonModal :is-open="showImagePostInput" 
            v-on:ion-modal-did-dismiss="closeModal">
    <IonHeader>
      <IonToolbar>
        <IonTitle>Image Post Input</IonTitle>
      </IonToolbar>
    </IonHeader>
    <IonContent class="ion-padding">
      <IonItem>
        <IonLabel position="floating">Title</IonLabel>
        <IonInput v-model="data.title" />
      </IonItem>
      <IonItem>
        <IonLabel position="floating">Content</IonLabel>
        <IonInput v-model="data.content" />
      </IonItem>
      <div v-if="imageURL" style="margin: auto; width: 50%; margin-top: 18px">
        <ion-img :src="imageURL" />
      </div>
      <div style="float: right; margin: 12px">
        <IonButton @click="closeModal" color="danger">CANCEL</IonButton>
        <IonButton @click="onSubmit">SAVE</IonButton>
      </div>
    </IonContent>
  </IonModal>
</template>
```

Code section

```tsx
defineProps({
  // flag to show/hide the modal
  showImagePostInput: {
    type: Boolean,
    default: false,
  },
  // image URL data
  imageURL: {
    type: String,
    default: "",
  },
});

// events emmitted by the component
const emit = defineEmits<{
  // event to close the modal
  (event: "image-post-cancel"): void;
  // event to save the data
  (
    event: "image-post-submit",
    { title, content }: { title: string; content: string }
  ): void;
}>();

// data from the component form
const data = ref({
  title: "",
  content: "",
});

/**
 * close modal take no action
 */
const closeModal = () => {
  emit("image-post-cancel");
};

/**
 * close modal and pass form data
 */
const onSubmit = () => {
  emit("image-post-submit", {
    title: data.value.title,
    content: data.value.content,
  });
};
```

### Capacitor Configuration and Setup

```bash
$ npm install --save @capacitor/core @capacitor/cli
$ npx cap init
```

Add your platform that you want to use, Android or IOS

```bash
$ npx cap add android
$ npx cap add ios
```

### Additional Configuration

Since we are using the camera we will need to install the camera component

```bash
npm install @capacitor/camera
npx cap sync
```

See link for instruction to install the capacitor camera plugin [Camera Capacitor Plugin API - Capacitor (capacitorjs.com)](https://capacitorjs.com/docs/apis/camera)

Add appropriate permissions in Android and IOS to access the camera

Android Changes - `AndroidManifest.xml`

```xml
<!-- Permissions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```

IOS Changes in `Info.plist`

```xml
  <key>NSPhotoLibraryUsageDescription</key>
	<string>Photo Useage Description</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>Photo Library Add Description</string>
	<key>NSCameraUsageDescription</key>
	<string>Useage Description</string>
```

## Running The Code

This code should now run fine as a PWA and provide a camera for you to take a photo and save to the data base.

if you want to deploy to a mobile device you need to have the backend running on a known ip address and then make sure you set that ip address in the nuxt configuration

```bash
runtimeConfig: {
    public: {
      API_URL: process.env.NODE_ENV === "development" ? "/api" : [YOUR SERVER IP ADDRESS],
    }
  },
```

now you can deploy the app to you mobile device and then start up your nuxt server and you should be good to go.

## Source Code
{% github aaronksaunders/nuxt-ionic-prisma-camera-app %}

## Related Links
- Module - https://ionic.roe.dev/
- VS Code Plugin - https://marketplace.visualstudio.com/items?itemName=ionic.ionic
- Ionic Vue - https://ionicframework.com/vue
- Capacitor - https://capacitorjs.com/docs/apis/camera
- Prisma - https://www.prisma.io/
- Volar Typescript Issue [FIXED] - https://github.com/ionic-team/ionic-framework/issues/24169#ionic 
- Source Code - https://github.com/aaronksaunders/nuxt-ionic-prisma-camera-app/