@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,686 lines (1,428 loc) • 44.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.apolloServerTemplate = void 0;
exports.apolloServerTemplate = {
id: 'apollo-server',
name: 'Apollo Server',
displayName: 'Apollo Server',
description: 'Community-driven, open-source GraphQL server with TypeScript, subscriptions, and caching',
language: 'typescript',
framework: 'apollo-server',
version: '4.10.0',
tags: ['graphql', 'apollo', 'subscriptions', 'caching', 'typescript'],
port: 4000,
features: [
'Apollo Server 4 with Express integration',
'TypeScript with full type definitions',
'GraphQL subscriptions with WebSockets',
'DataLoader for N+1 query optimization',
'File uploads with graphql-upload',
'Redis caching and rate limiting',
'Apollo Studio integration',
'Comprehensive testing setup'
],
dependencies: {
'@apollo/server': '^4.10.0',
'@apollo/server-plugin-response-cache': '^4.1.3',
'@apollo/server-plugin-landing-page-graphql-playground': '^4.0.1',
'express': '^4.18.2',
'cors': '^2.8.5',
'body-parser': '^1.20.2',
'graphql': '^16.8.1',
'graphql-subscriptions': '^2.0.0',
'graphql-ws': '^5.14.3',
'ws': '^8.16.0',
'dataloader': '^2.2.2',
'graphql-upload': '^16.0.2',
'redis': '^4.6.11',
'ioredis': '^5.3.2',
'jsonwebtoken': '^9.0.2',
'bcryptjs': '^2.4.3',
'uuid': '^9.0.1',
'dotenv': '^16.3.1',
'winston': '^3.11.0',
'graphql-rate-limit': '^3.3.0',
'graphql-depth-limit': '^1.1.0',
'graphql-cost-analysis': '^1.1.0'
},
devDependencies: {
'@types/node': '^20.10.4',
'@types/express': '^4.17.21',
'@types/cors': '^2.8.17',
'@types/jsonwebtoken': '^9.0.5',
'@types/bcryptjs': '^2.4.6',
'@types/ws': '^8.5.10',
'typescript': '^5.3.3',
'ts-node': '^10.9.2',
'tsx': '^4.7.0',
'nodemon': '^3.0.2',
'@types/jest': '^29.5.11',
'jest': '^29.7.0',
'ts-jest': '^29.1.1',
'supertest': '^6.3.3',
'@types/supertest': '^6.0.2'
},
files: {
'src/index.ts': `import 'dotenv/config';
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import cors from 'cors';
import bodyParser from 'body-parser';
import { createServer } from 'http';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { createContext } from './context';
import { plugins } from './plugins';
import { dataSources } from './datasources';
import { logger } from './utils/logger';
import { setupRedis } from './utils/redis';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.js';
async function startApolloServer() {
const app = express();
const httpServer = createServer(app);
// Initialize Redis
const redis = await setupRedis();
// Create GraphQL schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Create WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
// Set up WebSocket server
const serverCleanup = useServer(
{
schema,
context: async (ctx, msg, args) => {
return createContext({ req: ctx.extra.request, redis });
},
},
wsServer
);
// Create Apollo Server
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
...plugins,
],
formatError: (err) => {
logger.error('GraphQL Error:', err);
// Remove stack trace in production
if (process.env.NODE_ENV === 'production') {
delete err.extensions?.exception?.stacktrace;
}
return err;
},
});
await server.start();
// Apply middleware
app.use(
'/graphql',
cors<cors.CorsRequest>(),
bodyParser.json({ limit: '50mb' }),
graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }),
expressMiddleware(server, {
context: async ({ req }) => createContext({ req, redis, dataSources }),
})
);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 4000;
httpServer.listen(PORT, () => {
logger.info(\`🚀 Server ready at http://localhost:\${PORT}/graphql\`);
logger.info(\`🚀 Subscriptions ready at ws://localhost:\${PORT}/graphql\`);
});
}
startApolloServer().catch((err) => {
logger.error('Failed to start server:', err);
process.exit(1);
});`,
'src/schema/index.ts': `import { gql } from 'graphql-tag';
import { userTypeDefs } from './user';
import { postTypeDefs } from './post';
import { fileTypeDefs } from './file';
const baseTypeDefs = gql\`
type Query {
_empty: String
}
type Mutation {
_empty: String
}
type Subscription {
_empty: String
}
enum CacheControlScope {
PUBLIC
PRIVATE
}
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
directive @rateLimit(
window: String!
max: Int!
) on FIELD_DEFINITION
directive @auth(
requires: Role = USER
) on FIELD_DEFINITION
enum Role {
ADMIN
USER
GUEST
}
\`;
export const typeDefs = [
baseTypeDefs,
userTypeDefs,
postTypeDefs,
fileTypeDefs,
];`,
'src/schema/user.ts': `import { gql } from 'graphql-tag';
export const userTypeDefs = gql\`
extend type Query {
me: User @auth
user(id: ID!): User @cacheControl(maxAge: 60)
users(limit: Int = 10, offset: Int = 0): UserConnection!
}
extend type Mutation {
register(input: RegisterInput!): AuthPayload!
login(input: LoginInput!): AuthPayload!
updateProfile(input: UpdateProfileInput!): User! @auth
changePassword(input: ChangePasswordInput!): User! @auth
}
extend type Subscription {
userStatusChanged(userId: ID!): UserStatus!
}
type User {
id: ID!
username: String!
email: String!
profile: UserProfile
posts: [Post!]!
createdAt: String!
updatedAt: String!
}
type UserProfile {
bio: String
avatar: String
location: String
website: String
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type AuthPayload {
token: String!
user: User!
}
type UserStatus {
userId: ID!
status: String!
lastSeen: String!
}
input RegisterInput {
username: String!
email: String!
password: String!
}
input LoginInput {
email: String!
password: String!
}
input UpdateProfileInput {
bio: String
avatar: String
location: String
website: String
}
input ChangePasswordInput {
currentPassword: String!
newPassword: String!
}
\`;`,
'src/schema/post.ts': `import { gql } from 'graphql-tag';
export const postTypeDefs = gql\`
extend type Query {
post(id: ID!): Post @cacheControl(maxAge: 300)
posts(
limit: Int = 20
offset: Int = 0
orderBy: PostOrderBy = CREATED_AT_DESC
): PostConnection! @rateLimit(window: "1m", max: 100)
searchPosts(query: String!): [Post!]!
}
extend type Mutation {
createPost(input: CreatePostInput!): Post! @auth
updatePost(id: ID!, input: UpdatePostInput!): Post! @auth
deletePost(id: ID!): Boolean! @auth
likePost(id: ID!): Post! @auth @rateLimit(window: "1m", max: 30)
}
extend type Subscription {
postAdded: Post!
postUpdated(id: ID!): Post!
postLiked(id: ID!): PostLikeEvent!
}
type Post {
id: ID!
title: String!
content: String!
excerpt: String!
author: User!
tags: [String!]!
likes: Int!
likedBy: [User!]!
comments: [Comment!]!
createdAt: String!
updatedAt: String!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PostLikeEvent {
post: Post!
user: User!
totalLikes: Int!
}
enum PostOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
LIKES_ASC
LIKES_DESC
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
\`;`,
'src/schema/file.ts': `import { gql } from 'graphql-tag';
export const fileTypeDefs = gql\`
scalar Upload
extend type Mutation {
uploadFile(file: Upload!): File! @auth
uploadFiles(files: [Upload!]!): [File!]! @auth
deleteFile(id: ID!): Boolean! @auth
}
type File {
id: ID!
filename: String!
mimetype: String!
encoding: String!
url: String!
size: Int!
uploadedBy: User!
createdAt: String!
}
\`;`,
'src/resolvers/index.ts': `import { userResolvers } from './user';
import { postResolvers } from './post';
import { fileResolvers } from './file';
import { GraphQLUpload } from 'graphql-upload/GraphQLUpload.js';
export const resolvers = {
Upload: GraphQLUpload,
Query: {
...userResolvers.Query,
...postResolvers.Query,
},
Mutation: {
...userResolvers.Mutation,
...postResolvers.Mutation,
...fileResolvers.Mutation,
},
Subscription: {
...userResolvers.Subscription,
...postResolvers.Subscription,
},
User: userResolvers.User,
Post: postResolvers.Post,
};`,
'src/resolvers/user.ts': `import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { GraphQLError } from 'graphql';
import { withFilter } from 'graphql-subscriptions';
import { pubsub } from '../utils/pubsub';
const USER_STATUS_CHANGED = 'USER_STATUS_CHANGED';
export const userResolvers = {
Query: {
me: async (_: any, __: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
return dataSources.userAPI.findById(user.id);
},
user: async (_: any, { id }: any, { dataSources }: any) => {
return dataSources.userAPI.findById(id);
},
users: async (_: any, { limit, offset }: any, { dataSources }: any) => {
return dataSources.userAPI.findAll({ limit, offset });
},
},
Mutation: {
register: async (_: any, { input }: any, { dataSources }: any) => {
const existingUser = await dataSources.userAPI.findByEmail(input.email);
if (existingUser) {
throw new GraphQLError('User already exists');
}
const hashedPassword = await bcrypt.hash(input.password, 10);
const user = await dataSources.userAPI.create({
...input,
password: hashedPassword,
});
const token = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
return { token, user };
},
login: async (_: any, { input }: any, { dataSources }: any) => {
const user = await dataSources.userAPI.findByEmail(input.email);
if (!user) {
throw new GraphQLError('Invalid credentials');
}
const valid = await bcrypt.compare(input.password, user.password);
if (!valid) {
throw new GraphQLError('Invalid credentials');
}
const token = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
// Publish status change
pubsub.publish(USER_STATUS_CHANGED, {
userStatusChanged: {
userId: user.id,
status: 'online',
lastSeen: new Date().toISOString(),
},
});
return { token, user };
},
updateProfile: async (_: any, { input }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
return dataSources.userAPI.updateProfile(user.id, input);
},
changePassword: async (_: any, { input }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const currentUser = await dataSources.userAPI.findById(user.id);
const valid = await bcrypt.compare(input.currentPassword, currentUser.password);
if (!valid) {
throw new GraphQLError('Current password is incorrect');
}
const hashedPassword = await bcrypt.hash(input.newPassword, 10);
return dataSources.userAPI.update(user.id, { password: hashedPassword });
},
},
Subscription: {
userStatusChanged: {
subscribe: withFilter(
() => pubsub.asyncIterator([USER_STATUS_CHANGED]),
(payload, variables) => {
return payload.userStatusChanged.userId === variables.userId;
}
),
},
},
User: {
posts: async (parent: any, _: any, { loaders }: any) => {
return loaders.postsByUserLoader.load(parent.id);
},
},
};`,
'src/resolvers/post.ts': `import { GraphQLError } from 'graphql';
import { withFilter } from 'graphql-subscriptions';
import { pubsub } from '../utils/pubsub';
const POST_ADDED = 'POST_ADDED';
const POST_UPDATED = 'POST_UPDATED';
const POST_LIKED = 'POST_LIKED';
export const postResolvers = {
Query: {
post: async (_: any, { id }: any, { dataSources }: any) => {
return dataSources.postAPI.findById(id);
},
posts: async (_: any, { limit, offset, orderBy }: any, { dataSources }: any) => {
return dataSources.postAPI.findAll({ limit, offset, orderBy });
},
searchPosts: async (_: any, { query }: any, { dataSources }: any) => {
return dataSources.postAPI.search(query);
},
},
Mutation: {
createPost: async (_: any, { input }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await dataSources.postAPI.create({
...input,
authorId: user.id,
});
// Publish to subscribers
pubsub.publish(POST_ADDED, { postAdded: post });
return post;
},
updatePost: async (_: any, { id, input }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await dataSources.postAPI.findById(id);
if (!post) throw new GraphQLError('Post not found');
if (post.authorId !== user.id) {
throw new GraphQLError('Not authorized to update this post');
}
const updatedPost = await dataSources.postAPI.update(id, input);
// Publish to subscribers
pubsub.publish(POST_UPDATED, { postUpdated: updatedPost });
return updatedPost;
},
deletePost: async (_: any, { id }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await dataSources.postAPI.findById(id);
if (!post) throw new GraphQLError('Post not found');
if (post.authorId !== user.id) {
throw new GraphQLError('Not authorized to delete this post');
}
return dataSources.postAPI.delete(id);
},
likePost: async (_: any, { id }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const post = await dataSources.postAPI.like(id, user.id);
// Publish to subscribers
pubsub.publish(POST_LIKED, {
postLiked: {
post,
user,
totalLikes: post.likes,
},
});
return post;
},
},
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator([POST_ADDED]),
},
postUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator([POST_UPDATED]),
(payload, variables) => {
return payload.postUpdated.id === variables.id;
}
),
},
postLiked: {
subscribe: withFilter(
() => pubsub.asyncIterator([POST_LIKED]),
(payload, variables) => {
return payload.postLiked.post.id === variables.id;
}
),
},
},
Post: {
author: async (parent: any, _: any, { loaders }: any) => {
return loaders.userLoader.load(parent.authorId);
},
likedBy: async (parent: any, _: any, { loaders }: any) => {
return loaders.likedByLoader.load(parent.id);
},
comments: async (parent: any, _: any, { loaders }: any) => {
return loaders.commentsByPostLoader.load(parent.id);
},
excerpt: (parent: any) => {
return parent.content.substring(0, 150) + '...';
},
},
};`,
'src/resolvers/file.ts': `import { GraphQLError } from 'graphql';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs/promises';
import path from 'path';
const UPLOAD_DIR = path.join(process.cwd(), 'uploads');
// Ensure upload directory exists
fs.mkdir(UPLOAD_DIR, { recursive: true }).catch(console.error);
export const fileResolvers = {
Mutation: {
uploadFile: async (_: any, { file }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const { createReadStream, filename, mimetype, encoding } = await file;
// Validate file type
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!allowedMimeTypes.includes(mimetype)) {
throw new GraphQLError('File type not allowed');
}
// Generate unique filename
const fileId = uuidv4();
const ext = path.extname(filename);
const newFilename = \`\${fileId}\${ext}\`;
const filePath = path.join(UPLOAD_DIR, newFilename);
// Save file
const stream = createReadStream();
const writeStream = await fs.open(filePath, 'w');
let size = 0;
for await (const chunk of stream) {
size += chunk.length;
// Limit file size to 10MB
if (size > 10 * 1024 * 1024) {
await writeStream.close();
await fs.unlink(filePath);
throw new GraphQLError('File too large');
}
await writeStream.write(chunk);
}
await writeStream.close();
// Save file metadata to database
const fileRecord = await dataSources.fileAPI.create({
id: fileId,
filename,
mimetype,
encoding,
size,
url: \`/uploads/\${newFilename}\`,
uploadedById: user.id,
});
return fileRecord;
},
uploadFiles: async (_: any, { files }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const uploadPromises = files.map(async (file: any) => {
return fileResolvers.Mutation.uploadFile(_, { file }, { user, dataSources });
});
return Promise.all(uploadPromises);
},
deleteFile: async (_: any, { id }: any, { user, dataSources }: any) => {
if (!user) throw new GraphQLError('Not authenticated');
const file = await dataSources.fileAPI.findById(id);
if (!file) throw new GraphQLError('File not found');
if (file.uploadedById !== user.id) {
throw new GraphQLError('Not authorized to delete this file');
}
// Delete physical file
const filePath = path.join(process.cwd(), file.url);
await fs.unlink(filePath).catch(() => {}); // Ignore if file doesn't exist
// Delete from database
return dataSources.fileAPI.delete(id);
},
},
};`,
'src/context.ts': `import jwt from 'jsonwebtoken';
import { Request } from 'express';
import { createLoaders } from './loaders';
interface Context {
user?: any;
loaders: any;
redis: any;
dataSources?: any;
}
export async function createContext({
req,
redis,
dataSources,
}: {
req: Request;
redis: any;
dataSources?: any;
}): Promise<Context> {
// Get the user token from headers
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
user = jwt.verify(token, process.env.JWT_SECRET!);
} catch (err) {
// Invalid token
}
}
// Create DataLoader instances for this request
const loaders = createLoaders();
return {
user,
loaders,
redis,
dataSources,
};
}`,
'src/loaders/index.ts': `import DataLoader from 'dataloader';
import { UserAPI } from '../datasources/UserAPI';
import { PostAPI } from '../datasources/PostAPI';
export function createLoaders() {
const userAPI = new UserAPI();
const postAPI = new PostAPI();
return {
userLoader: new DataLoader<string, any>(async (userIds) => {
const users = await userAPI.findByIds(userIds as string[]);
const userMap = new Map(users.map((user: any) => [user.id, user]));
return userIds.map((id) => userMap.get(id));
}),
postsByUserLoader: new DataLoader<string, any>(async (userIds) => {
const posts = await postAPI.findByUserIds(userIds as string[]);
const postsMap = new Map<string, any[]>();
posts.forEach((post: any) => {
if (!postsMap.has(post.authorId)) {
postsMap.set(post.authorId, []);
}
postsMap.get(post.authorId)!.push(post);
});
return userIds.map((id) => postsMap.get(id) || []);
}),
commentsByPostLoader: new DataLoader<string, any>(async (postIds) => {
// Simulate fetching comments
return postIds.map(() => []);
}),
likedByLoader: new DataLoader<string, any>(async (postIds) => {
// Simulate fetching users who liked posts
return postIds.map(() => []);
}),
};
}`,
'src/datasources/index.ts': `import { UserAPI } from './UserAPI';
import { PostAPI } from './PostAPI';
import { FileAPI } from './FileAPI';
export const dataSources = () => ({
userAPI: new UserAPI(),
postAPI: new PostAPI(),
fileAPI: new FileAPI(),
});`,
'src/datasources/UserAPI.ts': `import { RESTDataSource } from '@apollo/datasource-rest';
// This is a mock implementation. Replace with actual database queries.
export class UserAPI extends RESTDataSource {
private users = new Map();
async findById(id: string) {
return this.users.get(id);
}
async findByIds(ids: string[]) {
return ids.map(id => this.users.get(id)).filter(Boolean);
}
async findByEmail(email: string) {
return Array.from(this.users.values()).find((user: any) => user.email === email);
}
async findAll({ limit = 10, offset = 0 }) {
const allUsers = Array.from(this.users.values());
const edges = allUsers.slice(offset, offset + limit).map((user: any) => ({
node: user,
cursor: Buffer.from(user.id).toString('base64'),
}));
return {
edges,
pageInfo: {
hasNextPage: offset + limit < allUsers.length,
hasPreviousPage: offset > 0,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: allUsers.length,
};
}
async create(input: any) {
const user = {
id: Date.now().toString(),
...input,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.users.set(user.id, user);
return user;
}
async update(id: string, input: any) {
const user = this.users.get(id);
if (!user) throw new Error('User not found');
const updated = {
...user,
...input,
updatedAt: new Date().toISOString(),
};
this.users.set(id, updated);
return updated;
}
async updateProfile(id: string, input: any) {
const user = this.users.get(id);
if (!user) throw new Error('User not found');
user.profile = { ...user.profile, ...input };
user.updatedAt = new Date().toISOString();
return user;
}
}`,
'src/datasources/PostAPI.ts': `import { RESTDataSource } from '@apollo/datasource-rest';
export class PostAPI extends RESTDataSource {
private posts = new Map();
private likes = new Map(); // postId -> Set of userIds
async findById(id: string) {
return this.posts.get(id);
}
async findByUserIds(userIds: string[]) {
return Array.from(this.posts.values()).filter((post: any) =>
userIds.includes(post.authorId)
);
}
async findAll({ limit = 20, offset = 0, orderBy = 'CREATED_AT_DESC' }) {
let allPosts = Array.from(this.posts.values());
// Sort posts
allPosts.sort((a: any, b: any) => {
switch (orderBy) {
case 'CREATED_AT_ASC':
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
case 'CREATED_AT_DESC':
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
case 'LIKES_ASC':
return a.likes - b.likes;
case 'LIKES_DESC':
return b.likes - a.likes;
default:
return 0;
}
});
const edges = allPosts.slice(offset, offset + limit).map((post: any) => ({
node: post,
cursor: Buffer.from(post.id).toString('base64'),
}));
return {
edges,
pageInfo: {
hasNextPage: offset + limit < allPosts.length,
hasPreviousPage: offset > 0,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: allPosts.length,
};
}
async search(query: string) {
const lowercaseQuery = query.toLowerCase();
return Array.from(this.posts.values()).filter((post: any) =>
post.title.toLowerCase().includes(lowercaseQuery) ||
post.content.toLowerCase().includes(lowercaseQuery)
);
}
async create(input: any) {
const post = {
id: Date.now().toString(),
...input,
likes: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
this.posts.set(post.id, post);
return post;
}
async update(id: string, input: any) {
const post = this.posts.get(id);
if (!post) throw new Error('Post not found');
const updated = {
...post,
...input,
updatedAt: new Date().toISOString(),
};
this.posts.set(id, updated);
return updated;
}
async delete(id: string) {
return this.posts.delete(id);
}
async like(postId: string, userId: string) {
const post = this.posts.get(postId);
if (!post) throw new Error('Post not found');
if (!this.likes.has(postId)) {
this.likes.set(postId, new Set());
}
const postLikes = this.likes.get(postId)!;
if (postLikes.has(userId)) {
postLikes.delete(userId);
post.likes = Math.max(0, post.likes - 1);
} else {
postLikes.add(userId);
post.likes += 1;
}
return post;
}
}`,
'src/datasources/FileAPI.ts': `import { RESTDataSource } from '@apollo/datasource-rest';
export class FileAPI extends RESTDataSource {
private files = new Map();
async findById(id: string) {
return this.files.get(id);
}
async create(input: any) {
const file = {
...input,
createdAt: new Date().toISOString(),
};
this.files.set(file.id, file);
return file;
}
async delete(id: string) {
return this.files.delete(id);
}
}`,
'src/plugins/index.ts': `import responseCachePlugin from '@apollo/server-plugin-response-cache';
import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground';
import { rateLimitPlugin } from './rateLimit';
import { depthLimitPlugin } from './depthLimit';
import { costAnalysisPlugin } from './costAnalysis';
import { loggingPlugin } from './logging';
export const plugins = [
responseCachePlugin({
sessionId: ({ request }) =>
request.http?.headers.get('authorization') || 'anonymous',
}),
process.env.NODE_ENV !== 'production' &&
ApolloServerPluginLandingPageGraphQLPlayground(),
rateLimitPlugin(),
depthLimitPlugin(5),
costAnalysisPlugin({ maximumCost: 1000 }),
loggingPlugin(),
].filter(Boolean);`,
'src/plugins/rateLimit.ts': `import { GraphQLError } from 'graphql';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
export function rateLimitPlugin() {
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
return {
async requestDidStart() {
return {
async willSendResponse() {
// Clean up old entries periodically
const now = Date.now();
for (const [key, value] of rateLimitMap.entries()) {
if (value.resetTime < now) {
rateLimitMap.delete(key);
}
}
},
};
},
};
}
export function rateLimitDirectiveTransformer(schema: any) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const rateLimitDirective = getDirective(schema, fieldConfig, 'rateLimit')?.[0];
if (rateLimitDirective) {
const { window, max } = rateLimitDirective;
const originalResolve = fieldConfig.resolve || defaultFieldResolver;
fieldConfig.resolve = async function (source, args, context, info) {
const key = \`\${context.user?.id || context.ip}:\${info.fieldName}\`;
// Parse window (e.g., "1m" -> 60000ms)
const windowMs = parseWindow(window);
const now = Date.now();
const limit = rateLimitMap.get(key);
if (!limit || limit.resetTime < now) {
rateLimitMap.set(key, { count: 1, resetTime: now + windowMs });
} else if (limit.count >= max) {
throw new GraphQLError(\`Rate limit exceeded. Try again in \${Math.ceil((limit.resetTime - now) / 1000)} seconds.\`);
} else {
limit.count++;
}
return originalResolve(source, args, context, info);
};
}
return fieldConfig;
},
});
}
function parseWindow(window: string): number {
const unit = window.slice(-1);
const value = parseInt(window.slice(0, -1));
switch (unit) {
case 's': return value * 1000;
case 'm': return value * 60 * 1000;
case 'h': return value * 60 * 60 * 1000;
default: throw new Error(\`Invalid window format: \${window}\`);
}
}`,
'src/plugins/depthLimit.ts': `import depthLimit from 'graphql-depth-limit';
export function depthLimitPlugin(maxDepth: number) {
return {
async requestDidStart() {
return {
async didResolveOperation(requestContext: any) {
const errors = depthLimit(maxDepth)(requestContext.document);
if (errors) {
throw errors;
}
},
};
},
};
}`,
'src/plugins/costAnalysis.ts': `import costAnalysis from 'graphql-cost-analysis';
export function costAnalysisPlugin(options: any) {
return {
async requestDidStart() {
return {
async didResolveOperation(requestContext: any) {
const cost = costAnalysis({
...options,
query: requestContext.request.query,
variables: requestContext.request.variables,
});
if (cost > options.maximumCost) {
throw new Error(\`Query cost \${cost} exceeds maximum cost \${options.maximumCost}\`);
}
},
};
},
};
}`,
'src/plugins/logging.ts': `import { logger } from '../utils/logger';
export function loggingPlugin() {
return {
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse(requestContext: any) {
const duration = Date.now() - start;
const { request, response } = requestContext;
logger.info('GraphQL Request', {
query: request.query,
variables: request.variables,
duration: \`\${duration}ms\`,
errors: response.body.singleResult.errors,
});
},
async didEncounterErrors(requestContext: any) {
logger.error('GraphQL Errors', {
errors: requestContext.errors,
});
},
};
},
};
}`,
'src/utils/logger.ts': `import winston from 'winston';
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
}),
],
});`,
'src/utils/pubsub.ts': `import { PubSub } from 'graphql-subscriptions';
// In production, use Redis PubSub for scalability
export const pubsub = new PubSub();`,
'src/utils/redis.ts': `import Redis from 'ioredis';
import { logger } from './logger';
export async function setupRedis() {
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
});
redis.on('connect', () => {
logger.info('Connected to Redis');
});
redis.on('error', (err) => {
logger.error('Redis error:', err);
});
return redis;
}`,
'src/directives/auth.ts': `import { GraphQLError } from 'graphql';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
export function authDirectiveTransformer(schema: any) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
if (authDirective) {
const { requires } = authDirective;
const originalResolve = fieldConfig.resolve || defaultFieldResolver;
fieldConfig.resolve = async function (source, args, context, info) {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (requires && context.user.role !== requires && requires !== 'USER') {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return originalResolve(source, args, context, info);
};
}
return fieldConfig;
},
});
}`,
'src/tests/server.test.ts': `import request from 'supertest';
import { ApolloServer } from '@apollo/server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { typeDefs } from '../schema';
import { resolvers } from '../resolvers';
describe('Apollo Server', () => {
let server: ApolloServer;
beforeAll(async () => {
const schema = makeExecutableSchema({ typeDefs, resolvers });
server = new ApolloServer({ schema });
await server.start();
});
afterAll(async () => {
await server.stop();
});
describe('Queries', () => {
it('should execute a simple query', async () => {
const query = \`
query {
__typename
}
\`;
const result = await server.executeOperation({ query });
expect(result.body.kind).toBe('single');
expect(result.body.singleResult.errors).toBeUndefined();
});
});
describe('Mutations', () => {
it('should register a new user', async () => {
const mutation = \`
mutation RegisterUser($input: RegisterInput!) {
register(input: $input) {
token
user {
id
username
email
}
}
}
\`;
const variables = {
input: {
username: 'testuser',
email: 'test@example.com',
password: 'password123',
},
};
const result = await server.executeOperation({
query: mutation,
variables
});
expect(result.body.kind).toBe('single');
expect(result.body.singleResult.data?.register).toBeDefined();
expect(result.body.singleResult.data?.register.token).toBeDefined();
expect(result.body.singleResult.data?.register.user.email).toBe('test@example.com');
});
});
describe('Error Handling', () => {
it('should handle authentication errors', async () => {
const query = \`
query {
me {
id
username
}
}
\`;
const result = await server.executeOperation({ query });
expect(result.body.kind).toBe('single');
expect(result.body.singleResult.errors).toBeDefined();
expect(result.body.singleResult.errors?.[0].message).toBe('Not authenticated');
});
});
});`,
'docker-compose.yml': `version: '3.8'
services:
app:
build: .
ports:
- "4000:4000"
environment:
- NODE_ENV=development
- JWT_SECRET=your-secret-key
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- redis
- postgres
volumes:
- ./uploads:/app/uploads
- ./src:/app/src
- ./package.json:/app/package.json
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
postgres:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=apollo
- POSTGRES_PASSWORD=apollo
- POSTGRES_DB=apollo_db
volumes:
- postgres-data:/var/lib/postgresql/data
apollo-studio:
image: apollographql/apollo-studio:latest
ports:
- "4001:4001"
environment:
- APOLLO_KEY=\${APOLLO_KEY}
- APOLLO_GRAPH_REF=\${APOLLO_GRAPH_REF}
volumes:
redis-data:
postgres-data:`,
'Dockerfile': `FROM node:20-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Build TypeScript
RUN npm run build
# Create uploads directory
RUN mkdir -p uploads
EXPOSE 4000
CMD ["node", "dist/index.js"]`,
'.env.example': `NODE_ENV=development
PORT=4000
# JWT Secret
JWT_SECRET=your-super-secret-jwt-key
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# Database Configuration
DATABASE_URL=postgresql://apollo:apollo@localhost:5432/apollo_db
# Apollo Studio (optional)
APOLLO_KEY=
APOLLO_GRAPH_REF=
# File Upload
MAX_FILE_SIZE=10485760
UPLOAD_DIR=./uploads`,
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage"]
}`,
'package.json': `{
"name": "apollo-server-app",
"version": "1.0.0",
"description": "Apollo Server GraphQL API with TypeScript",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"format": "prettier --write src/**/*.ts",
"generate": "graphql-codegen",
"studio": "rover dev"
},
"keywords": ["apollo", "graphql", "typescript", "api"],
"author": "",
"license": "MIT"
}`,
'jest.config.js': `module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
};`,
'README.md': `# Apollo Server GraphQL API
A production-ready GraphQL API built with Apollo Server 4, TypeScript, and modern best practices.
## Features
- **Apollo Server 4** with Express integration
- **TypeScript** for type safety
- **GraphQL Subscriptions** with WebSockets
- **DataLoader** for N+1 query optimization
- **File Uploads** with graphql-upload
- **Redis Caching** for performance
- **Rate Limiting** per field
- **Authentication** with JWT
- **Error Handling** and logging
- **Testing** with Jest
- **Docker** support
- **Apollo Studio** integration
## Getting Started
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Copy environment variables:
\`\`\`bash
cp .env.example .env
\`\`\`
3. Start Redis and PostgreSQL:
\`\`\`bash
docker-compose up -d redis postgres
\`\`\`
4. Run in development mode:
\`\`\`bash
npm run dev
\`\`\`
5. Open GraphQL Playground:
http://localhost:4000/graphql
## GraphQL Schema
### Queries
- \`me\`: Get current user
- \`user(id: ID!)\`: Get user by ID
- \`users(limit: Int, offset: Int)\`: Get paginated users
- \`post(id: ID!)\`: Get post by ID
- \`posts(limit: Int, offset: Int, orderBy: PostOrderBy)\`: Get paginated posts
- \`searchPosts(query: String!)\`: Search posts
### Mutations
- \`register(input: RegisterInput!)\`: Register new user
- \`login(input: LoginInput!)\`: Login user
- \`createPost(input: CreatePostInput!)\`: Create new post
- \`updatePost(id: ID!, input: UpdatePostInput!)\`: Update post
- \`deletePost(id: ID!)\`: Delete post
- \`likePost(id: ID!)\`: Like/unlike post
- \`uploadFile(file: Upload!)\`: Upload single file
- \`uploadFiles(files: [Upload!]!)\`: Upload multiple files
### Subscriptions
- \`userStatusChanged(userId: ID!)\`: User status updates
- \`postAdded\`: New posts
- \`postUpdated(id: ID!)\`: Post updates
- \`postLiked(id: ID!)\`: Post likes
## Authentication
Include JWT token in Authorization header:
\`\`\`
Authorization: Bearer <your-jwt-token>
\`\`\`
## Rate Limiting
Fields can be rate limited using the \`@rateLimit\` directive:
\`\`\`graphql
posts(...): [Post!]! @rateLimit(window: "1m", max: 100)
\`\`\`
## Caching
Use the \`@cacheControl\` directive for caching:
\`\`\`graphql
user(id: ID!): User @cacheControl(maxAge: 60)
\`\`\`
## File Uploads
Upload files using the \`Upload\` scalar:
\`\`\`graphql
mutation UploadFile($file: Upload!) {
uploadFile(file: $file) {
id
filename
url
}
}
\`\`\`
## Testing
Run tests:
\`\`\`bash
npm test
npm run test:coverage
\`\`\`
## Docker
Build and run with Docker:
\`\`\`bash
docker-compose up
\`\`\`
## Production
1. Build for production:
\`\`\`bash
npm run build
\`\`\`
2. Set environment variables
3. Run with PM2 or similar process manager
## Apollo Studio
Connect to Apollo Studio for monitoring:
1. Set \`APOLLO_KEY\` and \`APOLLO_GRAPH_REF\` in environment
2. Access Studio at https://studio.apollographql.com
`
},
scripts: {
postInstall: `echo "Apollo Server setup complete! Run 'npm run dev' to start the server."`
}
};