Starter kit for Node.js + Typescript + React.js + Apollo GraphQL + TypeORM
- Typescript Node + React + pnpm
- Apollo GraphQL (apollo-client 3.0)
- TypeORM w/ testing connections (Docker-Compose Postgres + GitHub Actions)
- GraphQL Code Generator
- Material UI
- Unit/Integration/E2E tests
- React client with Hooks
- Prettier + ESLint configuration
NOTE: VS Code settings for ESLint+Prettier (consequence of mono repo structure)
"eslint.workingDirectories": [ "./client", "./server" ]
Requires Node.js 18.19 or higher, Postgres 11+ required for database. Docker-compose provided for Postgres. Should be easy to adapt examples to other databases... may update database support in future versions.
This is setup like a mono-repo with seperate folders for clients and server, each with their own package and config. You could set these up in their own repos, switch to each folder to start the respective packages.
-
client - Vite-based React 19 application with Material UI, Apollo Client 3, and GraphQL Code Generator for type-safe hooks plus modern tooling like Vitest.
-
server - Apollo Server using GraphQL Code Generator for resolvers + types. Using TypeORM for database access, working examples of relay style cursor pagination, unit, integration and e2e tests.
BREAKING CHANGE: Migrated from Yarn to pnpm workspaces
- Migration to pnpm v9.6.0: Switched from Yarn to pnpm for better performance, disk space efficiency, and modern workspace features
- Updated GitHub Actions: All CI/CD workflows now use pnpm with proper workspace support and PostgreSQL services
- Modern TypeScript 5.8.2: Implemented catalog dependencies across workspaces for consistent versioning
- Enhanced Scripts: Added comprehensive development scripts including
dev,clean,typecheck, and proper workspace filtering - Improved Configuration: Added modern
.npmrcwith optimal pnpm settings for monorepo development - Removed Yarn Artifacts: Cleaned up all Yarn-specific files (.yarnrc.yml, .yarn directories, lock files)
- Install pnpm globally:
npm install -g pnpmor use Corepack:corepack enable pnpm - Install dependencies:
pnpm install - Start development:
pnpm dev(runs both client and server) - Build all packages:
pnpm build - Run tests:
pnpm test
pnpm dev:client- Start only the React clientpnpm dev:server- Start only the Node.js serverpnpm clean- Clean all node_modules and build artifactspnpm typecheck- Run TypeScript type checking across all packages
This section provides details about the available GraphQL queries and mutations, based on the schema in server/src/graphql/.
NodeInterface: Represents an object with anid,created, andupdatedtimestamp. Implemented byCardandCategory.id: ID!created: DateTime!updated: DateTime
PageInfoType: Provides information about pagination.hasNextPage: Boolean!hasPreviousPage: Boolean!startCursor: StringendCursor: StringtotalCount: Int!
DirectionEnumEnum: Specifies the direction for ordering.ASC(Ascending)DESC(Descending)
- Scalar Types:
Date: Represents a date.DateTime: Represents a date and time.Time: Represents a time.
- Description: Retrieves a paginated list of cards. Supports filtering by category and ordering.
- Parameters:
first(Int): Returns the firstncards.last(Int): Returns the lastncards (requiresbefore).after(String): Returns cards after the specified cursor.before(String): Returns cards before the specified cursor.orderByColumn(String): Column to order by (e.g., "label", "created"). Defaults to "id" if not specified or invalid.orderByDirection(DirectionEnum): Direction of ordering (ASCorDESC). Defaults toASC.categoryId(ID): Filters cards by a specific category ID.
- Returns:
CardConnection!: An object containing a list of cards (edges) and pagination information (pageInfo).CardConnection:pageInfo: PageInfo!edges: [CardEdge!]!CardEdge:cursor: String!node: Card!(seeCardtype below)
- Example: Get the first 10 cards, ordered by label descending.
query GetFirstTenCardsOrdered {
cards(first: 10, orderByColumn: "label", orderByDirection: DESC) {
pageInfo {
hasNextPage
endCursor
totalCount
}
edges {
cursor
node {
id
number
label
description
created
categories {
id
name
}
}
}
}
}- Description: Retrieves a single card by its unique ID.
- Parameters:
id(ID!): The unique identifier of the card.
- Returns:
Card!: The card object if found.Card(implementsNode):id: ID!number: Intlabel: Stringdescription: Stringcreated: DateTime!updated: DateTimecategories: [Category!]!(List of categories associated with the card)
- Example:
query GetCardById($cardId: ID!) {
card(id: $cardId) {
id
number
label
description
created
updated
categories {
id
name
}
}
}- Description: Retrieves a list of categories. Supports filtering by card IDs and ordering.
- Parameters:
cardIds(String): A comma-separated string of card IDs to filter categories that are associated with these cards.orderByColumn(String): Column to order by (e.g., "name", "created"). Defaults to "id" if not specified or invalid.orderByDirection(DirectionEnum): Direction of ordering (ASCorDESC). Defaults toASC.
- Returns:
[Category!]!: A list of category objects.Category(implementsNode):id: ID!name: Stringcreated: DateTime!updated: DateTimecards(...): A connection to retrieve cards associated with this category (supports same pagination/ordering as the top-levelcardsquery).
- Example: Get all categories, ordered by name.
query GetAllCategoriesOrdered {
categories(orderByColumn: "name", orderByDirection: ASC) {
id
name
created
# Example of fetching cards for each category (first 2)
cards(first: 2) {
edges {
node {
id
label
}
}
totalCount
}
}
}- Description: Retrieves a single category by its unique ID.
- Parameters:
id(ID!): The unique identifier of the category.
- Returns:
Category!: The category object if found. (SeeCategorytype definition above). - Example:
query GetCategoryById($categoryId: ID!) {
category(id: $categoryId) {
id
name
created
updated
cards { # Fetch all cards in this category
edges {
node {
id
label
}
}
}
}
}- Description: Retrieves any object that implements the
Nodeinterface by its global ID. This can be aCardor aCategory. - Parameters:
id(ID!): The global unique identifier of the node.
- Returns:
Node!: The node object if found. You can use inline fragments to get specific fields based on the type. - Example:
query GetNode($nodeId: ID!) {
node(id: $nodeId) {
id
created
updated
... on Card {
label
description
number
categories {
id
name
}
}
... on Category {
name
cards {
totalCount
}
}
}
}- Description: Adds a new card.
- Parameters:
input(CardInput!): The details for the new card.CardInput:number(Int)label(String)description(String)categoryId(ID): Optional ID of the category to associate with this card.
- Returns:
CardsUpdatedResponse!:CardsUpdatedResponse:success: Boolean!message: String!card: Card!(The newly created card)
- Example:
mutation AddNewCard($newCard: CardInput!) {
addCard(input: $newCard) {
success
message
card {
id
label
number
description
created
categories {
id
name
}
}
}
}
# Example variables for the above mutation:
# {
# "newCard": {
# "label": "New Task Card",
# "description": "Details about the new task.",
# "number": 101,
# "categoryId": "some-category-id" # Optional
# }
# }- Description: Updates an existing card by its ID.
- Parameters:
id(ID!): The ID of the card to update.input(CardInput!): The new details for the card. Fields inCardInputare optional for updates.CardInput: (Same asaddCard)
- Returns:
CardsUpdatedResponse!: (Same asaddCard, butcardis the updated card) - Example:
mutation ModifyCard($cardId: ID!, $updatedData: CardInput!) {
updateCard(id: $cardId, input: $updatedData) {
success
message
card {
id
label
description
updated
}
}
}
# Example variables for the above mutation:
# {
# "cardId": "existing-card-id",
# "updatedData": {
# "label": "Updated Task Card Label"
# }
# }- Description: Removes a card by its ID.
- Parameters:
id(ID!): The ID of the card to remove.
- Returns:
CardsUpdatedResponse!: (Same asaddCard,cardwill be the removed card details) - Example:
mutation DeleteCard($cardId: ID!) {
removeCard(id: $cardId) {
success
message
card { # Details of the card that was removed
id
label
}
}
}- Description: Adds a new category.
- Parameters:
input(CategoryInput!): The details for the new category.CategoryInput:name(String)
- Returns:
CategoryUpdatedResponse!:CategoryUpdatedResponse:success: Boolean!message: String!category: Category!(The newly created category)
- Example:
mutation AddNewCategory($newCategory: CategoryInput!) {
addCategory(input: $newCategory) {
success
message
category {
id
name
created
}
}
}
# Example variables for the above mutation:
# {
# "newCategory": {
# "name": "Project Alpha"
# }
# }- Description: Updates an existing category by its ID.
- Parameters:
id(ID!): The ID of the category to update.input(CategoryInput!): The new details for the category.CategoryInput: (Same asaddCategory)
- Returns:
CategoryUpdatedResponse!: (Same asaddCategory, butcategoryis the updated category) - Example:
mutation ModifyCategory($categoryId: ID!, $updatedData: CategoryInput!) {
updateCategory(id: $categoryId, input: $updatedData) {
success
message
category {
id
name
updated
}
}
}
# Example variables for the above mutation:
# {
# "categoryId": "existing-category-id",
# "updatedData": {
# "name": "Project Beta Features"
# }
# }- Description: Removes a category by its ID.
- Parameters:
id(ID!): The ID of the category to remove.
- Returns:
CategoryUpdatedResponse!: (Same asaddCategory,categorywill be the removed category details) - Example:
mutation DeleteCategory($categoryId: ID!) {
removeCategory(id: $categoryId) {
success
message
category { # Details of the category that was removed
id
name
}
}
}The Brainstrike server follows a modern, layered architecture designed for scalability, maintainability, and type safety:
graph TB
%% External Layer
Client["π Client Applications<br/>(React/Next.js)"]
Studio["π οΈ Apollo Studio<br/>(GraphQL Playground)"]
%% API Gateway Layer
subgraph "π API Layer"
Express["β‘ Express Server<br/>(Port 4000)"]
CORS["π CORS Middleware<br/>(Security)"]
Apollo["π Apollo Server<br/>(GraphQL Gateway)"]
end
%% Business Logic Layer
subgraph "π§ Business Logic"
Schema["π GraphQL Schema<br/>(Type Definitions)"]
Resolvers["βοΈ GraphQL Resolvers<br/>(Query/Mutation Logic)"]
subgraph "π Data Sources"
CardAPI["π Card API<br/>(Business Logic)"]
CategoryAPI["π Category API<br/>(Business Logic)"]
end
end
%% Data Access Layer
subgraph "πΎ Data Access Layer"
TypeORM["π TypeORM<br/>(ORM Framework)"]
subgraph "π¦ Entities"
CardEntity["π Card Entity"]
CategoryEntity["π Category Entity"]
UserEntity["π€ User Entity"]
end
subgraph "π Database Operations"
Migrations["π Migrations<br/>(Schema Evolution)"]
Repositories["π Repositories<br/>(Data Access)"]
end
end
%% Database Layer
subgraph "ποΈ Database Layer"
PostgreSQL["π PostgreSQL<br/>(Primary Database)"]
TestDB["π§ͺ Test Database<br/>(brainstrike_test)"]
end
%% Development Tools
subgraph "π οΈ Development & Testing"
Vitest["β‘ Vitest<br/>(Unit Testing)"]
Faker["π Faker.js<br/>(Test Data)"]
Codegen["π§ GraphQL Codegen<br/>(Type Generation)"]
ESLint["π ESLint + Prettier<br/>(Code Quality)"]
end
%% Environment & Config
subgraph "βοΈ Configuration"
EnvConfig["π Environment Config<br/>(.env files)"]
ORMConfig["π§ ORM Configuration<br/>(Database Settings)"]
end
%% Connections
Client --> Express
Studio --> Express
Express --> CORS
CORS --> Apollo
Apollo --> Schema
Apollo --> Resolvers
Resolvers --> CardAPI
Resolvers --> CategoryAPI
CardAPI --> TypeORM
CategoryAPI --> TypeORM
TypeORM --> CardEntity
TypeORM --> CategoryEntity
TypeORM --> UserEntity
TypeORM --> Repositories
TypeORM --> Migrations
Repositories --> PostgreSQL
Migrations --> PostgreSQL
Vitest --> TestDB
Faker --> TestDB
EnvConfig --> ORMConfig
ORMConfig --> TypeORM
Codegen --> Schema
%% Styling with black text
classDef clientLayer fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000
classDef apiLayer fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000000
classDef businessLayer fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px,color:#000000
classDef dataLayer fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000000
classDef dbLayer fill:#fce4ec,stroke:#880e4f,stroke-width:2px,color:#000000
classDef devLayer fill:#f1f8e9,stroke:#33691e,stroke-width:2px,color:#000000
classDef configLayer fill:#e0f2f1,stroke:#004d40,stroke-width:2px,color:#000000
class Client,Studio clientLayer
class Express,CORS,Apollo apiLayer
class Schema,Resolvers,CardAPI,CategoryAPI businessLayer
class TypeORM,CardEntity,CategoryEntity,UserEntity,Migrations,Repositories dataLayer
class PostgreSQL,TestDB dbLayer
class Vitest,Faker,Codegen,ESLint devLayer
class EnvConfig,ORMConfig configLayer
- π Client Layer: React/Next.js applications and Apollo Studio for development
- π API Layer: Express.js with Apollo Server providing a robust GraphQL gateway
- π§ Business Logic: Clean separation with dedicated data source APIs and resolvers
- πΎ Data Access: TypeORM with entity models and repository patterns
- ποΈ Database: PostgreSQL with separate test database for development
- π οΈ Development: Comprehensive testing and code generation tools
- βοΈ Configuration: Environment-based configuration management
This architecture ensures type safety, scalability, and maintainability while following modern best practices for GraphQL APIs.

