GraphQL Types & Schema Design — Complete Guide with SDL Examples
The GraphQL type system defines your API’s shape — every field, relationship, and argument is described in the schema using Schema Definition Language (SDL).
What You’ll Learn
- Object types, scalars, and custom scalars
- Enums, interfaces, and union types
- Input types for mutations and arguments
- Non-null and list modifiers
- Schema design patterns and best practices
Why Types and Schema Matter
The schema is the contract between client and server. A well-designed schema makes your API intuitive, evolvable, and self-documenting. DodaTech’s Durga Antivirus Pro GraphQL schema defines types for Device, Threat, Scan, User, and Alert — each with clear relationships that let the dashboard team build UIs without backend coordination.
flowchart LR
A["SDL Schema"] --> B["Object Types\n(User, Device)"]
A --> C["Scalars\n(String, Int, ID)"]
A --> D["Enums\n(Severity, Status)"]
A --> E["Interfaces\n(Node)"]
A --> F["Input Types\n(CreateDeviceInput)"]
style A fill:#dbeafe,stroke:#2563eb
Object Types
Object types are the most common building block. They represent entities with multiple fields:
type Device {
id: ID!
name: String!
os: String!
version: String
lastScan: DateTime
user: User!
threats: [Threat!]!
}Each field has a type (String, ID, DateTime) and optional modifiers (! for non-null, [] for lists).
Scalar Types
Built-in Scalars
| Scalar | Description | Example |
|---|---|---|
String | UTF-8 text | "Office-PC" |
Int | 32-bit integer | 42 |
Float | Double-precision | 3.14 |
Boolean | True or false | true |
ID | Unique identifier | "dev-001" |
Custom Scalars
Custom scalars add validation for specialized data:
scalar DateTime
scalar JSON
scalar EmailAddress
scalar URLconst { GraphQLScalarType, Kind } = require('graphql');
const dateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time string',
serialize(value) { return value instanceof Date ? value.toISOString() : value; },
parseValue(value) { return new Date(value); },
parseLiteral(ast) {
return ast.kind === Kind.STRING ? new Date(ast.value) : null;
},
});Why custom scalars? They validate data at the API boundary, ensuring every DateTime field is a valid ISO 8601 string and every EmailAddress is a valid email.
Enums
Enums restrict a field to a fixed set of values:
enum Severity {
LOW
MEDIUM
HIGH
CRITICAL
}
enum DeviceStatus {
ONLINE
OFFLINE
QUARANTINED
DELETED
}Usage:
type Threat {
severity: Severity!
status: ThreatStatus!
}
# Query with enum argument
query CriticalThreats {
threats(severity: CRITICAL) { id name }
}Enums are more explicit than strings — they prevent typos, enable autocomplete in GraphiQL, and make the schema self-documenting.
Non-Null and List Modifiers
Modifiers control whether a field can be null or return multiple values:
| Syntax | Meaning | Can be null? |
|---|---|---|
String | Nullable string | Yes |
String! | Non-null string | No |
[String] | Nullable list of nullable strings | Yes (list and items) |
[String!] | Nullable list of non-null strings | List yes, items no |
[String]! | Non-null list of nullable strings | List no, items yes |
[String!]! | Non-null list of non-null strings | Neither |
Best practice: Use [String!]! for most list fields — the list always exists but may be empty, and items are never null.
Interfaces
Interfaces define shared fields that multiple types can implement:
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type Device implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
os: String!
}
type Threat implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
severity: Severity!
}Queries can return interface types and use inline fragments to access type-specific fields:
query Search {
search(query: "Emotet") {
... on Device { name os }
... on Threat { name severity }
}
}Union Types
Unions represent a field that can be one of several types (no shared fields required):
union SearchResult = Device | Threat | User
type Query {
search(query: String!): [SearchResult!]!
}Use inline fragments to handle each type:
query {
search(query: "critical") {
__typename # Always check which type was returned
... on Device { id name os }
... on Threat { id name severity detectedAt }
... on User { id email }
}
}Input Types
Input types pass complex objects as arguments to mutations:
input CreateDeviceInput {
name: String!
os: String!
version: String
userId: ID!
}
input UpdateDeviceInput {
name: String
os: String
version: String
}
type Mutation {
createDevice(input: CreateDeviceInput!): Device!
updateDevice(id: ID!, input: UpdateDeviceInput!): Device!
}Key difference: Object types use type and can have resolvers. Input types use input and cannot have resolvers — they’re pure data structures for arguments.
Common Mistakes
1. Overusing Nullable Fields
Marking everything as nullable (String instead of String!) makes clients handle null checks everywhere. Be intentional — if a field always has a value, make it non-null.
2. Exposing Database IDs
Exposing auto-increment database IDs (userId: 123) is a security risk. Use opaque identifiers (UUIDs or GraphQL’s ID scalar).
3. Not Using Input Types
Passing 10+ arguments directly to a mutation is unwieldy. Use input types to group related fields.
4. Ignoring Deprecation
Instead of breaking changes, deprecate fields:
type Device {
os: String
operatingSystem: String @deprecated(reason: "Use 'os' instead")
}5. Making Enums Too Generic
Status { ACTIVE, INACTIVE, PENDING } is too vague for reuse across devices, threats, and users. Create specific enums for each domain.
Practice Questions
- What is the difference between
typeandinputin SDL? - When would you use an interface vs a union?
- What does
[String!]!mean? - Why use custom scalars like
DateTimeinstead ofString? - How do you deprecate a field in GraphQL?
Answers:
typedefines output objects with resolvers.inputdefines argument structures without resolvers.- Use interfaces when types share common fields. Use unions when types are completely different but can appear in the same field.
- A non-nullable list containing non-nullable strings. The list is always present (possibly empty), and every item is guaranteed to be a string.
- Custom scalars validate format at the API boundary, provide better documentation, and enable tooling (autocomplete, validation).
- Add
@deprecated(reason: "Use fieldName instead")directive to the field.
Challenge: Design a GraphQL schema for Durga Antivirus Pro’s alert system. Include types for Alert (id, message, severity, device, createdAt), AlertSeverity enum, an AlertConnection type for pagination, input types for creating and updating alerts, and queries/mutations for listing, creating, and dismissing alerts.
FAQ
Try It Yourself
Create a schema with multiple types and test it:
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
enum Severity { LOW MEDIUM HIGH CRITICAL }
type Threat { id: ID! name: String! severity: Severity! }
type Query { threats: [Threat!]! }
`;
const resolvers = {
Query: { threats: () => [
{ id: '1', name: 'Emotet', severity: 'CRITICAL' },
{ id: '2', name: 'Adware', severity: 'LOW' },
]},
};
new ApolloServer({ typeDefs, resolvers }).listen(4000);What’s Next
| Topic | Description |
|---|---|
| Queries & Resolvers | Resolver functions, arguments, and context |
| Mutations & Input Types | Creating and modifying data |
| GraphQL Architecture | Server setup, batching, and caching |
| GraphQL Introduction | Review core concepts |
What’s Next
Congratulations on completing this Graphql Types Schema tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro