How to Build a GraphQL API with Prisma and Deploy to DigitalOcean App Platform

Creating a GraphQL API leveraging Prisma is one of the most effective methods for developing type-safe, database-oriented applications. When paired with the user-friendly deployment features of DigitalOcean App Platform, this combination yields a robust technology stack adept at managing everything from database tasks to live hosting. In this guide, we will explore the steps necessary to establish a full GraphQL API using Prisma as your ORM, ensure correct database connections, and deploy the entire setup to the DigitalOcean App Platform with automated builds and scaling.
How Prisma Integrates with GraphQL
GraphQL functions as a query language, allowing clients to request precisely the data they require, while Prisma operates as a modern ORM, efficiently crafting type-safe database queries. This synergy produces a strong API framework where Prisma manages database tasks and GraphQL offers the data interaction interface.
Prisma creates a client reflecting your database schema, providing autocompletion and type safety across your application. When you outline your data models in the Prisma schema file, it establishes the necessary database tables and generates corresponding TypeScript types that align with your schema.
The standard process for requests operates as follows: GraphQL receives a query, resolvers manage the request utilising Prisma client methods, Prisma converts these into optimised SQL commands, and the database sends back results that return through the same pathway to your client.
Initial Setup and Configuration
Begin by creating a new de.js project and adding the essential dependencies. You will require GraphQL libraries, Prisma tools, and a server framework such as Apollo server.
mkdir graphql-prisma-api
cd graphql-prisma-api
npm init -y
npm install @apollo/server graphql prisma @prisma/client
npm install -D typescript @types/de ts-de demon
Initialise Prisma within your project, which will generate the necessary configuration files and a foundational schema:
npx prisma init
This command produces a prisma
directory containing a schema.prisma
file and adds a .env
file for your database connection string. Update your schema file with a sample data model:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Create a TypeScript configuration file to manage module resolution and compilation:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Creating the GraphQL Schema and Resolvers
Define your GraphQL schema that specifies the structure of the API. This schema should correlate with your Prisma models but can exhibit alternative fields or additional computed properties:
// src/schema.ts
export const typeDefs = `
type User {
id: Int!
email: String!
name: String
posts: [Post!]!
createdAt: String!
updatedAt: String!
}
type Post {
id: Int!
title: String!
content: String
published: Boolean!
author: User!
createdAt: String!
updatedAt: String!
}
type Query {
users: [User!]!
user(id: Int!): User
posts: [Post!]!
post(id: Int!): Post
publishedPosts: [Post!]!
}
type Mutation {
createUser(email: String!, name: String): User!
createPost(title: String!, content: String, authorId: Int!): Post!
publishPost(id: Int!): Post
deletePost(id: Int!): Post
}
`;
Implement resolvers that utilise the Prisma client to interact with your database. These functions manage the actual data retrieval and manipulation:
// src/resolvers.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const resolvers = {
Query: {
users: async () => {
return await prisma.user.findMany({
include: { posts: true }
});
},
user: async (_: any, { id }: { id: number }) => {
return await prisma.user.findUnique({
where: { id },
include: { posts: true }
});
},
posts: async () => {
return await prisma.post.findMany({
include: { author: true }
});
},
post: async (_: any, { id }: { id: number }) => {
return await prisma.post.findUnique({
where: { id },
include: { author: true }
});
},
publishedPosts: async () => {
return await prisma.post.findMany({
where: { published: true },
include: { author: true }
});
}
},
Mutation: {
createUser: async (_: any, { email, name }: { email: string; name?: string }) => {
return await prisma.user.create({
data: { email, name },
include: { posts: true }
});
},
createPost: async (_: any, { title, content, authorId }: { title: string; content?: string; authorId: number }) => {
return await prisma.post.create({
data: { title, content, authorId },
include: { author: true }
});
},
publishPost: async (_: any, { id }: { id: number }) => {
return await prisma.post.update({
where: { id },
data: { published: true },
include: { author: true }
});
},
deletePost: async (_: any, { id }: { id: number }) => {
return await prisma.post.delete({
where: { id },
include: { author: true }
});
}
}
};
Configuring the Apollo server
Craft the primary server file that melds your schema and resolvers with Apollo server. This manages the GraphQL query processing and offers the GraphQL Playground for development:
// src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
async function startServer() {
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: parseInt(process.env.PORT || '4000') },
context: async ({ req }) => {
// Add authentication logic here if applicable
return { req };
},
});
console.log(🚀 server running at ${url}
);
}
startServer().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
Incorporate npm scripts in your package.json for development and production builds:
// package.json scripts section
{
"scripts": {
"dev": "demon --exec ts-de src/server.ts",
"build": "tsc",
"start": "de dist/server.js",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio"
}
}
Database Setup for Local Development
For local development, you can employ a PostgreSQL database via Docker or connect to a cloud database. Create a docker-compose.yml file to set up local PostgreSQL:
// docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15
restart: always
environment:
POSTGRES_DB: graphql_api
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Modify your .env file with the database connection string:
# .env
DATABASE_URL="postgresql://postgres:password@localhost:5432/graphql_api"
PORT=4000
Generate the Prisma client and push your schema to the database:
docker-compose up -d
npx prisma generate
npx prisma db push
Initiate your development server and test the API:
npm run dev
Navigate to http://localhost:4000
to access Apollo Studio and experiment with your GraphQL queries and mutations.
Preparing for Deployment on DigitalOcean App Platform
DigitalOcean App Platform necessitates specific configurations for a smooth deployment. Create an app specification file that delineates your application structure:
// .do/app.yaml
name: graphql-prisma-api
services:
- environment_slug: de-js
github:
branch: main
deploy_on_push: true
repo: your-username/your-repo-name
name: api
routes:
- path: /
run_command: npm start
build_command: npm run build && npx prisma generate
instance_count: 1
instance_size_slug: basic-xxs
source_dir: /
envs:
- key: DATABASE_URL
scope: RUN_TIME
type: SECRET
- key: PORT
scope: RUN_TIME
value: "8080"
databases:
- engine: PG
name: main-db
num_des: 1
size: db-s-dev-database
version: "13"
Create a build script to manage Prisma client generation during deployment:
// scripts/build.sh
#!/bin/bash
set -e
echo "Installing dependencies..."
npm ci
echo "Building TypeScript..."
npm run build
echo "Generating Prisma client..."
npx prisma generate
echo "Build completed successfully!"
Make the script executable and update your package.json:
chmod +x scripts/build.sh
// Update package.json
{
"scripts": {
"build": "tsc && npx prisma generate",
"start": "de dist/server.js",
"postinstall": "npx prisma generate"
}
}
Deploying on DigitalOcean App Platform
Upload your code to a Git repository and link it to DigitalOcean App Platform. This platform facilitates automatic deployments from GitHub, GitLab, or direct Git repositories.
To create a new application on DigitalOcean App Platform through the control panel:
- Select “Create App” from the Apps section
- Pick your Git provider and repository
- Select the branch for deployment (generally main or master)
- Configure build and run commands in accordance with your app.yaml file
- Add a managed PostgreSQL database or utilise an external connection
The platform will automatically recognise de.js projects and propose the necessary settings. Override these if you’re operating with custom configurations:
Setting
Development
Production
Build Command
npm run build
npm run build && npx prisma generate
Run Command
npm run dev
npm start
Environment
de.js 18+
de.js 18+ (LTS)
Instance Size
Basic
Basic or Professional
Set environment variables in the App Platform dashboard. The DATABASE_URL should point to your managed database or external PostgreSQL instance. DigitalOcean will automatically provide the connection string for managed databases.
Once deployed, run database migrations using the App Platform console or include migration commands in your build process:
// For initial deployment, run migrations manually
npx prisma migrate deploy
// Or include in build process
"build": "tsc && npx prisma generate && npx prisma migrate deploy"
Optimising Performance and Best Practices
Utilising GraphQL with Prisma may result in complex queries that could negatively affect database performance. It's crucial to employ strategies for query optimisation to ensure satisfactory response times:
// Implement connection limits and restrict query depth
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
{
requestDidStart() {
return {
willSendResponse(requestContext) {
// Log queries that take a long time
if (requestContext.request.http?.headers?.get('x-response-time')) {
console.log('Query duration:', requestContext.request.http.headers.get('x-response-time'));
}
},
};
},
},
],
});
Leverage Prisma's built-in connection pooling and enhance database queries through proper indexing:
// Update schema.prisma for enhanced performance
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
@@index([createdAt])
}
model Post {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
@@index([published, createdAt])
@@index([authorId])
}
Implement appropriate error handling and logging for production environments:
// src/utils/errors.ts
export class GraphQLError extends Error {
extensions: Record;
constructor(message: string, code: string, statusCode: number = 400) {
super(message);
this.extensions = {
code,
http: { status: statusCode }
};
}
}
// In resolvers
user: async (_: any, { id }: { id: number }) => {
try {
const user = await prisma.user.findUnique({
where: { id },
include: { posts: true }
});
if (!user) {
throw new GraphQLError('User not found', 'USER_NOT_FOUND', 404);
}
return user;
} catch (error) {
console.error('Error retrieving user:', error);
throw error;
}
}
Troubleshooting Common Issues
Database connection problems are some of the most prevalent issues faced during deployment. DigitalOcean App Platform requires specific SSL configurations for PostgreSQL connections:
# Update DATABASE_URL for DigitalOcean managed databases
DATABASE_URL="postgresql://username:password@host:port/database?sslmode=require"
Failures in Prisma client generation during the build can occur if dependencies are not installed correctly. Ensure your build command includes client generation:
// Common build issues and solutions
Problem: "PrismaClient cannot run in the browser"
Solution: Ensure prisma generate runs post npm install
Problem: "Environment variable not found: DATABASE_URL"
Solution: Verify environment variable settings in App Platform
Problem: "Migration failure during deployment"
Solution: Use prisma migrate deploy instead of prisma migrate dev
Memory issues might arise with large GraphQL queries. Consider implementing query complexity analysis and request size limits:
npm install graphql-query-complexity graphql-depth-limit
// Add to server configuration
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-query-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(10),
createComplexityLimitRule(1000)
],
});
Potential Use Cases and Extensions
This GraphQL API framework is suitable for a variety of application types. Blog platforms benefit from the established relationship between posts and users, while e-commerce applications can expand the schema to include products and orders:
// E-commerce extension example
model Product {
id Int @id @default(autoincrement())
name String
description String?
price Decimal @db.Decimal(10, 2)
inventory Int @default(0)
orders OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Order {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
items OrderItem[]
total Decimal @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
}
Authentication and authorization features can be integrated using JWT tokens and context-based access control:
// JWT authentication middleware
import jwt from 'jsonwebtoken';
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
let user = null;
if (req.headers.authorization) {
const token = req.headers.authorization.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
user = await prisma.user.findUnique({ where: { id: decoded.userId } });
} catch (error) {
console.warn('Token is invalid');
}
}
return { user, prisma };
},
});
The union of GraphQL, Prisma, and DigitalOcean App Platform creates a scalable base for contemporary web applications. Automatic scaling and managed database options render it suitable for projects ranging from modest personal initiatives to comprehensive enterprise systems.
For more sophisticated configurations, think about incorporating GraphQL subscriptions for real-time updates, using Prisma's advanced functionalities like aggregation and grouping, or integrating with DigitalOcean’s additional services, such as Spaces for file storage or Redis for caching.

This article includes references and materials from multiple online sources. We acknowledge and appreciate the contributions from all original authors, publishers, and websites. While every effort has been made to credit the source material accurately, any unintentional omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes your copyright, please contact us immediately for review and appropriate action.
This article is intended solely for informational and educational purposes and does not infringe upon the rights of copyright owners. If any copyrighted material has been used without proper attribution or in violation of copyright laws, it was unintentional, and we will correct it promptly upon notification. Please note that the redistribution or reproduction of any portion or all of the contents in any form is prohibited without the express written permission from the author and website owner. For permissions or further inquiries, please reach out to us.