Steve Kinney

Full-Stack TypeScript

Type-Safe Database Access with Prisma

Below is a step‑by‑step tutorial that not only shows you how to set up Prisma with Express and TypeScript for a simple todo app but also highlights the changes you made—replacing your custom TaskClient with Prisma for full type safety. Let’s jump in.

Initialize Prisma

Initialize Prisma by running:

npx prisma init

This creates a prisma/ folder with a schema.prisma file. Edit the file to define your Task model:

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

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

model Task {
  id          Int      @id @default(autoincrement())
  title       String
  description String?
  completed   Boolean  @default(false)
}

Then run a migration and generate the Prisma client:

npx prisma migrate dev --name init
npx prisma generate

Integrating Prisma in Your Express Server

Previously, you used a custom TaskClient (and a Database type from SQLite) to handle your database operations. Now we’re switching to Prisma to get true type safety. Here’s a look at your diff with changes:

diff --git a/server/src/server.ts b/server/src/server.ts
index bbdd593..53ee07a 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -1,8 +1,10 @@
 import cors from 'cors';
 import express from 'express';
-import type { Database } from 'sqlite';
 import swaggerUi from 'swagger-ui-express';

+import { PrismaClient } from '@prisma/client';
+const prisma = new PrismaClient();
+

Replacing CRUD Methods

Instead of calling methods on your custom client, you now use Prisma’s methods:

GET /tasks

const tasks = await prisma.task.findMany();

GET /tasks/:id

const task = await prisma.task.findUnique({ where: { id } });

POST /tasks

await prisma.task.create({ data: task });

PUT /tasks/:id

const data = req.body;
await prisma.task.update({ where: { id }, data });

DELETE /tasks/:id

await prisma.task.delete({ where: { id } });

6. Full Updated server.ts Example

Here’s what your complete server.ts might look like after integrating Prisma:

import cors from 'cors';
import express from 'express';
import swaggerUi from 'swagger-ui-express';

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

import {
	NewTaskSchema,
	TaskParamsSchema,
	TaskQuerySchema,
	UpdateTaskSchema,
} from 'busy-bee-schema';

import { handleError } from './handle-error.js';
import { openApiDocument } from './openapi.js';
import { validate } from './validate.js';

export async function createServer() {
	const app = express();
	app.use(cors());
	app.use(express.json());

	// Serve OpenAPI docs
	app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiDocument));

	// Expose OpenAPI spec as JSON
	app.get('/openapi.json', (req, res) => {
		res.json(openApiDocument);
	});

	app.get('/tasks', validate({ query: TaskQuerySchema }), async (req, res) => {
		try {
			const tasks = await prisma.task.findMany();
			return res.json(tasks);
		} catch (error) {
			return handleError(req, res, error);
		}
	});

	// Get a specific task
	app.get('/tasks/:id', validate({ params: TaskParamsSchema }), async (req, res) => {
		try {
			const { id } = req.params;
			const task = await prisma.task.findUnique({ where: { id } });

			if (!task) return res.status(404).json({ message: 'Task not found' });

			return res.json(task);
		} catch (error) {
			return handleError(req, res, error);
		}
	});

	app.post('/tasks', validate({ body: NewTaskSchema }), async (req, res) => {
		try {
			const task = req.body;
			await prisma.task.create({ data: task });
			return res.status(201).json({ message: 'Task created successfully' });
		} catch (error) {
			return handleError(req, res, error);
		}
	});

	// Update a task
	app.put(
		'/tasks/:id',
		validate({ params: TaskParamsSchema, body: UpdateTaskSchema }),
		async (req, res) => {
			try {
				const { id } = req.params;

				const data = req.body;
				await prisma.task.update({ where: { id }, data });

				return res.status(200).json({ message: 'Task updated successfully' });
			} catch (error) {
				return handleError(req, res, error);
			}
		},
	);

	// Delete a task
	app.delete('/tasks/:id', validate({ params: TaskParamsSchema }), async (req, res) => {
		try {
			const { id } = req.params;
			await prisma.task.delete({ where: { id } });
			return res.status(200).json({ message: 'Task deleted successfully' });
		} catch (error) {
			return handleError(req, res, error);
		}
	});

	return app;
}

Note: Make sure you handle type conversions if your route parameters (like id) need to be numbers. Depending on your Prisma schema, you might need to convert id from string to number (e.g., using Number(id)).

Last modified on .