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

Sharukhan PatanSharukhan Patan
5 min read

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.

GraphQL Playground and tried a mutation

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.

While building this project, I faced a couple of issues that might trip you up too. I've documented both in detail:

Feel free to jump into those if you hit the same bumps. These fixes saved me hours — hope they save you some too!

0
Subscribe to my newsletter

Read articles from Sharukhan Patan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sharukhan Patan
Sharukhan Patan