Software architecture

Implementing MVC with Next.js and Prisma

Date Published

Summary

M : Model = Prisma (data + repositories)

V : React = Server/client components

C : Controller = Route handlers + server actions

Why ? Keep your application simple and scalable

For this we'll use an example application


Application architecture

1. Usecase and class diagram

usecasediagram.png
classdiagram.png

2. Components diagram

components-3.png


The plantuml source of this diagram you can find here


What does MVC have to do with it ?

1. the M stands for model

This handles the DB configuration and the DB access via Prisma

1generator client {
2 provider = "prisma-client-js"
3 output = "../src/generated/prisma"
4}
5
6// prisma/schema.prisma
7datasource db {
8 provider = "postgresql"
9 url = env("DATABASE_URL")
10}
11
12model User {
13 id String @id @default(cuid())
14 email String @unique
15 hashedPassword String
16 posts Post[] // One User can have many Posts
17}
18
19model Post {
20 id String @id @default(cuid())
21 title String
22 slug String @unique
23 content String
24 published Boolean? @default(false)
25 imageUrl String?
26 author User @relation(fields: [authorId], references: [id]) // Each Post belongs to one User
27 authorId String
28 updatedAt DateTime @updatedAt
29 createdAt DateTime @default(now())
30
31 @@index(slug)
32}

Two models are defined : User and Post

2. The Controller part

In the controller layer, you define the actions (use cases) the application must perform.

In this application we have three actions :

createPost, editPost, deletePost : In our case, those also correspond to the features (use-cases) of the use-case diagram

in the componentsdiagram you will them see too at the bottom.

createPostCode :

1'use server';
2
3import prisma from '@/lib/db';
4import { revalidatePath } from 'next/cache';
5import { Prisma } from '@/generated/prisma';
6
7export async function createPost(formData: FormData) {
8 try {
9 await prisma.post.create({
10 data: {
11 title: formData.get('title') as string,
12 slug: (formData.get('title') as string)
13 .replace(/\s+/g, '-')
14 .toLowerCase(),
15 content: formData.get('content') as string,
16 author: {
17 connect: {
18 email: 'john@gmail.com',
19 },
20 },
21 },
22 });
23 } catch (error) {
24 if (error instanceof Prisma.PrismaClientKnownRequestError) {
25 console.log('error code :', error.code);
26 if (error.code === 'P2002') {
27 console.log(
28 'There is a unique constraint violation, a new user cannot be created with this email'
29 );
30 }
31 }
32 // console.error(error);
33 }
34 revalidatePath('/posts');

You see that the controller uses prisma (the model) to access the database.

3. The view part

This is the user interface of the application used by our actors (in the usecase diagram).
Those are the jsx components of our application. In the components diagram you see that the createPostForm component will use the createPost Action (or controller) to create a post.

createPostForm code

1import { createPost } from '@/actions/actions';
2
3export default function CreatePostForm() {
4 return (
5 <form
6 action={createPost}
7 className="flex flex-col gap-y-3 w-[300px] bg-white p-6 rounded-lg shadow-sm"
8 >
9 <input
10 type="text"
11 name="title"
12 placeholder="Title"
13 className="px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
14 required
15 />
16 <textarea
17 name="content"
18 rows={5}
19 placeholder="Content"
20 className="px-3 py-2 rounded-md border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
21 required
22 />
23 <button
24 type="submit"
25 className="bg-blue-600 hover:bg-blue-700 py-2 text-white rounded-md font-medium transition-colors"
26 >
27 Create Post
28 </button>
29 </form>
30 );
31}

4. This post is inspired by this nice video

thank you bytegrad !