GraphQL Starter Kit
Move beyond REST's over-fetching and under-fetching problems with a production-ready GraphQL server that's actually designed for scale. This kit gives you a fully structured schema with type-safe resolvers, N+1 query prevention via DataLoader, JWT authentication with field-level authorization, real-time subscriptions over WebSocket, and pagination patterns that work with real databases. Not a toy todo-app — a foundation for APIs that serve millions of queries.
Key Features
- Schema-First Design — Complete SDL schema with custom scalars (DateTime, Email, URL), input validation directives, and interface/union types for polymorphic data
- DataLoader Integration — Batch and cache database lookups to eliminate N+1 queries, with per-request scoped loaders that prevent stale data across requests
-
JWT Authentication Middleware — Token validation, role-based access control, and field-level
@auth(requires: ADMIN)directives - Subscription Support — WebSocket-based real-time updates with filtered subscriptions (subscribe to changes for specific entities)
-
Cursor-Based Pagination — Relay-style connections with
first/after/last/beforeargs, total counts, andpageInfowithhasNextPage - Error Handling Framework — Structured GraphQL errors with error codes, user-friendly messages, and debug extensions in development mode
- Query Complexity Analysis — Prevent abuse by calculating query cost before execution and rejecting queries that exceed a configurable threshold
Quick Start
- Review the schema definition:
# src/schema/schema.graphql
type Query {
user(id: ID!): User
users(first: Int = 10, after: String): UserConnection!
me: User @auth(requires: USER)
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
deleteUser(id: ID!): DeletePayload! @auth(requires: ADMIN)
}
type Subscription {
userCreated: User!
userUpdated(id: ID): User!
}
type User {
id: ID!
email: String!
name: String!
role: Role!
posts(first: Int = 10, after: String): PostConnection!
createdAt: DateTime!
}
enum Role { USER ADMIN MODERATOR }
- Start the server:
from graphql_starter_kit.server import create_app
from graphql_starter_kit.config import load_config
app = create_app(load_config("config.example.yaml"))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=4000)
- Query your API:
query GetUserWithPosts {
user(id: "42") {
name
email
posts(first: 5) {
edges {
node {
title
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
Architecture
graphql-starter-kit/
├── src/graphql_starter_kit/
│ ├── core.py # App factory, middleware chain
│ ├── server.py # ASGI server with WebSocket support
│ ├── schema/ # SDL schema, custom scalars, directives
│ ├── resolvers/ # Query, mutation, subscription, type resolvers
│ ├── dataloaders/ # Batched user/post loaders
│ ├── auth/ # JWT middleware, role-based permissions
│ ├── pagination.py # Cursor encode/decode, connection builder
│ └── utils.py # Error formatting, complexity calculator
├── examples/ # Sample queries and mutations
└── config.example.yaml
Request flow: HTTP/WebSocket -> Auth Middleware -> Complexity Check -> Resolver -> DataLoader (batched DB call) -> Response Formatting.
Usage Examples
DataLoader to Prevent N+1
# src/graphql_starter_kit/dataloaders/user_loader.py
class UserLoader:
"""Batches individual user lookups into a single query."""
def __init__(self, db):
self._db = db
self._cache: dict[str, dict] = {}
self._queue: list[str] = []
async def load(self, user_id: str) -> dict | None:
if user_id in self._cache:
return self._cache[user_id]
self._queue.append(user_id)
# Single batched query instead of N individual queries
ids = list(set(self._queue))
self._queue.clear()
rows = await self._db.fetch_many("SELECT * FROM users WHERE id = ANY($1)", ids)
for row in rows:
self._cache[str(row["id"])] = dict(row)
return self._cache.get(user_id)
Query Complexity Limiting
# Maximum query cost before rejection
# Each field has a cost of 1, list fields multiply by the `first` argument
# Example: user { posts(first: 10) { title } } = 1 + (10 * 1) = 11
MAX_QUERY_COST = 500
Configuration
| Key | Type | Default | Description |
|---|---|---|---|
server.port |
int | 4000 |
Server listen port |
server.debug |
bool | false |
Enable GraphiQL playground and debug errors |
auth.jwt_secret |
string | required | YOUR_JWT_SECRET_HERE |
auth.token_expiry_hours |
int | 24 |
JWT token lifetime |
query.max_complexity |
int | 500 |
Maximum allowed query cost |
query.max_depth |
int | 10 |
Maximum query nesting depth |
pagination.default_page_size |
int | 10 |
Default items per page |
pagination.max_page_size |
int | 100 |
Maximum items per page |
subscriptions.enabled |
bool | true |
Enable WebSocket subscriptions |
dataloader.cache_per_request |
bool | true |
Scope DataLoader cache to request |
Best Practices
-
Always use DataLoaders for nested relationships. Without them,
users { posts { author } }generates O(N*M) database queries. DataLoaders collapse these into 2-3 batched queries. -
Limit query depth AND complexity. Depth alone doesn't prevent
users(first: 1000) { posts(first: 1000) }. Complexity analysis multiplies list sizes through the tree. -
Return payloads from mutations, not bare types.
UserPayloadwithuseranderrorsfields lets clients handle partial failures gracefully. -
Version via schema evolution, not URL paths. Add fields freely, deprecate old fields with
@deprecated(reason: "..."), remove after zero usage confirmed.
Troubleshooting
N+1 queries still appearing in logs
Verify DataLoaders are instantiated per-request in your context factory, not globally. Check that resolvers call loader.load(id) instead of direct DB queries.
Subscription connection drops after 30 seconds
WebSocket connections need keepalive pings. Set --ws-ping-interval 20 for Uvicorn and proxy_read_timeout 300s in Nginx.
"Query complexity exceeds maximum" on simple queries
List fields without a first argument may default to a high assumed page size. Set explicit defaults in your schema (users(first: Int = 10)).
This is 1 of 7 resources in the API Developer Pro toolkit. Get the complete [GraphQL Starter Kit] with all files, templates, and documentation for $39.
Or grab the entire API Developer Pro bundle (7 products) for $79 — save 30%.
Top comments (0)