阿里云主机折上折
  • 微信号
Current Site:Index > Implementation of GraphQL in Express

Implementation of GraphQL in Express

Author:Chuan Chen 阅读数:4776人阅读 分类: Node.js

GraphQL, as a powerful API query language, is becoming increasingly popular in modern web development. Combined with the Express framework, it enables rapid construction of flexible data interfaces to meet the needs of frontend-backend separation. Below, we outline the specific implementation steps, from configuration and type definitions to resolvers and actual queries.

Environment Setup and Basic Configuration

First, install the necessary dependencies. After initializing the project with npm or yarn, install the following core packages:

npm install express express-graphql graphql

Basic Express server configuration:

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

const app = express();

app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true
}));

app.listen(4000, () => {
  console.log('Server running on http://localhost:4000/graphql');
});

Key points:

  • The graphqlHTTP middleware handles all GraphQL requests.
  • graphiql: true enables the interactive query interface.
  • The default port is 4000.

Type System Construction

The core of GraphQL is its strong type system. Below is an example defining data types for a blog system:

type Post {
  id: ID!
  title: String!
  content: String
  author: User
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post]
}

type Query {
  getPost(id: ID!): Post
  getAllPosts: [Post]
  getUser(id: ID!): User
}

Special syntax notes:

  • ! indicates a non-nullable field.
  • [] denotes an array type.
  • ID is a built-in scalar type in GraphQL.

Resolver Implementation

Each field requires a corresponding resolver function. Below are resolvers matching the above types:

const root = {
  getPost: ({ id }) => {
    return db.posts.find(post => post.id === id);
  },
  getAllPosts: () => db.posts,
  getUser: ({ id }) => {
    const user = db.users.find(user => user.id === id);
    user.posts = db.posts.filter(post => post.authorId === id);
    return user;
  },
  Post: {
    author: (parent) => db.users.find(user => user.id === parent.authorId)
  }
};

Nested resolver characteristics:

  • The parent object is passed as the parent parameter.
  • Data can be fetched asynchronously (simply return a Promise).
  • Each field can be resolved independently.

Query and Mutation Operations

Basic Query Example

A GraphQL query to fetch all post titles:

query {
  getAllPosts {
    title
    author {
      name
    }
  }
}

Example response structure:

{
  "data": {
    "getAllPosts": [
      {
        "title": "Introduction to GraphQL",
        "author": {
          "name": "John Doe"
        }
      }
    ]
  }
}

Query with Parameters

Fetching specific user information:

query GetUser($userId: ID!) {
  getUser(id: $userId) {
    name
    email
    posts {
      title
    }
  }
}

Corresponding query variables:

{
  "userId": "user123"
}

Data Mutation Operations

First, extend the schema:

type Mutation {
  createPost(title: String!, content: String): Post
  updatePost(id: ID!, title: String): Post
}

Then implement the corresponding resolvers:

const root = {
  // ...other resolvers...
  createPost: ({ title, content }) => {
    const newPost = { id: generateId(), title, content };
    db.posts.push(newPost);
    return newPost;
  },
  updatePost: ({ id, title }) => {
    const post = db.posts.find(p => p.id === id);
    if (title) post.title = title;
    return post;
  }
};

Example mutation call:

mutation {
  createPost(title: "New Post", content: "Content") {
    id
    title
  }
}

Advanced Features Implementation

Custom Scalar Types

Adding a date type requires the following steps:

  1. Define the scalar type:
scalar Date
  1. Implement serialization logic:
const { GraphQLScalarType } = require('graphql');
const DateScalar = new GraphQLScalarType({
  name: 'Date',
  serialize(value) {
    return new Date(value).toISOString();
  },
  parseValue(value) {
    return new Date(value);
  }
});
  1. Include it in schema resolution:
const schema = buildSchema(typeDefs);
schema._typeMap.Date = DateScalar;

Batch Query Optimization

Using DataLoader to solve the N+1 query problem:

const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.find({ id: { $in: userIds } });
  return userIds.map(id => users.find(u => u.id === id));
});

// Call in resolver
Post: {
  author: (parent) => userLoader.load(parent.authorId)
}

Subscription Implementation

  1. Install additional dependencies:
npm install graphql-subscriptions
  1. Create a PubSub instance:
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
  1. Extend the schema:
type Subscription {
  postCreated: Post
}
  1. Implement the resolver:
Subscription: {
  postCreated: {
    subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
  }
},
Mutation: {
  createPost: (args) => {
    const post = createPost(args);
    pubsub.publish('POST_CREATED', { postCreated: post });
    return post;
  }
}

Error Handling and Validation

Custom Error Formatting

Modify the graphqlHTTP configuration:

app.use('/graphql', graphqlHTTP({
  schema: schema,
  graphiql: true,
  customFormatErrorFn: (err) => {
    return {
      message: err.message,
      locations: err.locations,
      stack: process.env.NODE_ENV === 'development' ? err.stack : null
    };
  }
}));

Input Validation

Use custom directives for parameter validation:

directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

type Mutation {
  createPost(
    title: String! @length(max: 100)
    content: String @length(max: 1000)
  ): Post
}

Implement validation logic:

class LengthDirective extends SchemaDirectiveVisitor {
  visitInputFieldDefinition(field) {
    const { max } = this.args;
    field.type = new GraphQLNonNull(
      new GraphQLScalarType({
        name: `LengthAtMost${max}`,
        serialize: (value) => {
          if (value.length > max) {
            throw new Error(`Length cannot exceed ${max} characters`);
          }
          return value;
        }
      })
    );
  }
}

Performance Monitoring

Add Apollo Tracing support:

app.use('/graphql', graphqlHTTP({
  schema: schema,
  tracing: true,
  extensions: ({ document, variables, operationName, result }) => {
    return { 
      tracing: result.extensions?.tracing 
    };
  }
}));

The generated response will include detailed execution time data:

{
  "extensions": {
    "tracing": {
      "version": 1,
      "startTime": "2023-01-01T00:00:00.000Z",
      "endTime": "2023-01-01T00:00:00.020Z",
      "duration": 20000000,
      "execution": {
        "resolvers": [
          {
            "path": ["getAllPosts"],
            "parentType": "Query",
            "fieldName": "getAllPosts",
            "returnType": "[Post]",
            "startOffset": 1000000,
            "duration": 5000000
          }
        ]
      }
    }
  }
}

Recommended Project Structure for Real-World Applications

Recommended directory structure for medium-sized projects:

/src
  /graphql
    /types
      post.graphql
      user.graphql
    /resolvers
      post.resolver.js
      user.resolver.js
    schema.js
  /models
    post.model.js
    user.model.js
  server.js

Schema merging in schema.js:

const { mergeTypeDefs } = require('@graphql-tools/merge');
const fs = require('fs');
const path = require('path');

const typesArray = [];
fs.readdirSync(path.join(__dirname, 'types')).forEach(file => {
  typesArray.push(fs.readFileSync(path.join(__dirname, 'types', file), 'utf8'));
});

module.exports = mergeTypeDefs(typesArray);

Authentication Integration

JWT authentication middleware example:

const authMiddleware = async (req) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (token) {
    try {
      const user = jwt.verify(token, SECRET);
      return { user };
    } catch (e) {
      throw new AuthenticationError('Invalid token');
    }
  }
  return {};
};

app.use('/graphql', graphqlHTTP(async (req) => ({
  schema,
  context: await authMiddleware(req)
})));

Accessing user context in resolvers:

{
  Query: {
    myPosts: (_, args, context) => {
      if (!context.user) throw new Error('Unauthorized');
      return db.posts.filter(p => p.authorId === context.user.id);
    }
  }
}

File Upload Handling

  1. Install dependencies:
npm install graphql-upload
  1. Add middleware:
const { graphqlUploadExpress } = require('graphql-upload');

app.use(graphqlUploadExpress());
  1. Define upload type:
scalar Upload

type Mutation {
  uploadFile(file: Upload!): Boolean
}
  1. Implement resolver:
{
  Mutation: {
    uploadFile: async (_, { file }) => {
      const { createReadStream, filename } = await file;
      const stream = createReadStream();
      return new Promise((resolve, reject) => {
        stream.pipe(fs.createWriteStream(`./uploads/${filename}`))
          .on('finish', () => resolve(true))
          .on('error', reject);
      });
    }
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.