TypeScript + PostgreSQL + GraphQL in Node.js: Building a Fully Typed API from Scratch

I still remember the first time I needed to build a GraphQL API that was both scalable and type-safe. I’d used GraphQL before, but combining it with TypeScript and PostgreSQL added a whole new layer of structure and confidence to my development workflow. This stack has become one of my favorites, and today, I'm walking you through how I built a complete GraphQL API using Node.js, TypeScript, PostgreSQL, and Prisma.
This guide is ideal if you want the flexibility of GraphQL, the robustness of PostgreSQL, and the safety net that TypeScript brings — all without losing developer speed.
Why This Stack?
Choosing the right tools isn’t just about trends — it’s about solving real problems. Here’s why I landed on this setup:
GraphQL lets clients shape the data they need. No more over-fetching or juggling multiple endpoints.
PostgreSQL is reliable, powerful, and feature-rich — perfect for structured data with relationships.
TypeScript catches mistakes before they hit runtime. For APIs, that means fewer bugs and clearer contracts.
Prisma simplifies database interaction while maintaining full type safety.
Let me show you how I brought it all together.
Step 1: Project Setup
I started by creating a new folder and initializing the project:
mkdir ts-graphql-postgres
cd ts-graphql-postgres
npm init -y
Then I added the core dependencies:
npm install express graphql apollo-server-express
npm install prisma @prisma/client
npm install typescript ts-node-dev @types/node --save-dev
To configure TypeScript, I generated a tsconfig.json
file:
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Step 2: Setting Up Prisma and PostgreSQL
Prisma is my go-to ORM for TypeScript. It gives me a way to define database models that are tightly coupled with generated TypeScript types.
I initialized Prisma like this:
npx prisma init
Inside prisma/schema.prisma
, I defined the models:
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
authorId Int
author User @relation(fields: [authorId], references: [id])
}
And configured my PostgreSQL connection in the .env
file:
DATABASE_URL="postgresql://postgres:password@localhost:5432/mydb"
Then I ran the migration:
npx prisma migrate dev --name init
Step 3: Defining GraphQL Types
With Prisma ready, I wrote the GraphQL schema manually using Apollo Server’s gql
function:
export const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
users: [User!]
user(id: Int!): User
posts: [Post!]
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String, authorId: Int!): Post!
}
`;
Step 4: Implementing Resolvers
Resolvers are what link GraphQL operations to actual business logic. With Prisma already generating types, I had a smooth developer experience writing them:
export const resolvers = {
Query: {
users: () => prisma.user.findMany({ include: { posts: true } }),
user: (_: any, { id }: { id: number }) =>
prisma.user.findUnique({ where: { id }, include: { posts: true } }),
posts: () => prisma.post.findMany({ include: { author: true } }),
},
Mutation: {
createUser: (_: any, args: { name: string; email: string }) =>
prisma.user.create({ data: args }),
createPost: (_: any, args: { title: string; content?: string; authorId: number }) =>
prisma.post.create({ data: args }),
},
User: {
posts: (parent: any) => prisma.post.findMany({ where: { authorId: parent.id } }),
},
Post: {
author: (parent: any) => prisma.user.findUnique({ where: { id: parent.authorId } }),
}
};
Step 5: Bringing It Together with Apollo Server
In src/index.ts
, I wired everything together:
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
async function main() {
const app = express();
const server = new ApolloServer({
typeDefs,
resolvers,
});
await server.start();
server.applyMiddleware({ app });
app.listen(4000, () =>
console.log('Server running at http://localhost:4000/graphql')
);
}
main();
Step 6: Testing It Out
To test the API, I used GraphQL Playground and tried a mutation:
mutation {
createUser(name: "Jane Doe", email: "jane@example.com") {
id
name
}
}
And then queried the list of users:
query {
users {
name
posts {
title
}
}
}
Everything worked perfectly, with full type safety and schema validation.
Final Thoughts
This combination of Node.js, TypeScript, PostgreSQL, Prisma, and GraphQL has completely changed the way I build APIs. I no longer worry about mismatched data types or writing repetitive SQL. Everything is fast, flexible, and safe.
If you’re coming from REST and considering GraphQL, or just want a more robust backend development workflow, this stack is definitely worth exploring. Personally, the productivity boost and confidence I get from using TypeScript with Prisma alone is enough to keep me coming back.
Next up, I plan to add JWT authentication, custom error handling, and maybe even subscriptions for real-time updates. But that’s a post for another day.
Related Fixes You Might Need
While building this project, I faced a couple of issues that might trip you up too. I've documented both in detail:
💥 Ran into a version conflict between
express@5.1.0
andapollo-server-express@3.13.0
? Here’s how I resolved it →❗️Got an ESM import error when using Prisma with TypeScript? This small path tweak solved it for me →
⚠️ Error when adding a required column to an existing PostgreSQL table using Prisma? Check out my guide to handling that migration safely →
Feel free to jump into those if you hit the same bumps. These fixes saved me hours — hope they save you some too!
Subscribe to my newsletter
Read articles from Sharukhan Patan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
