Implementation of GraphQL in Express
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:
- Define the scalar type:
scalar Date
- 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);
}
});
- 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
- Install additional dependencies:
npm install graphql-subscriptions
- Create a PubSub instance:
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
- Extend the schema:
type Subscription {
postCreated: Post
}
- 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
- Install dependencies:
npm install graphql-upload
- Add middleware:
const { graphqlUploadExpress } = require('graphql-upload');
app.use(graphqlUploadExpress());
- Define upload type:
scalar Upload
type Mutation {
uploadFile(file: Upload!): Boolean
}
- 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
上一篇:Express与前端框架的集成