Saltearse al contenido

Guía de GraphQL

Aprende a construir APIs GraphQL poderosas con Veloce-TS v0.2.6.

Veloce-TS proporciona soporte completo de GraphQL con:

  • Generación Automática de Esquemas a partir de tipos TypeScript
  • Resolvers basados en Decoradores para código limpio y declarativo
  • Integración Zod para validación y seguridad de tipos
  • Argumentos con Seguridad de Tipos con validación automática
  • Playground e Introspección para desarrollo
import { VeloceTS } from 'veloce-ts';
const app = new VeloceTS();
app.install('graphql', {
path: '/graphql',
playground: true,
introspection: true
});
app.install('graphql', {
path: '/graphql', // Ruta del endpoint GraphQL
playground: true, // Habilitar GraphQL Playground
introspection: true, // Habilitar introspección de esquema
cors: true, // Habilitar CORS
context: (req) => ({ // Contexto personalizado
user: req.user,
db: database
})
});
import { Resolver, GQLQuery, Arg } from 'veloce-ts';
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
});
@Resolver()
export class UserResolver {
@GQLQuery()
async users(): Promise<User[]> {
return await userService.findAll();
}
@GQLQuery()
async user(@Arg('id', z.string()) id: string): Promise<User | null> {
return await userService.findById(id);
}
@GQLQuery()
async userByName(@Arg('name', z.string()) name: string): Promise<User[]> {
return await userService.findByName(name);
}
}
import { GQLMutation, Arg, Body } from 'veloce-ts';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: z.string().min(6)
});
@Resolver()
export class UserResolver {
@GQLMutation()
async createUser(@Arg('input', CreateUserSchema) input: z.infer<typeof CreateUserSchema>): Promise<User> {
const hashedPassword = await bcrypt.hash(input.password, 10);
const user = await userService.create({
...input,
password: hashedPassword
});
return user;
}
@GQLMutation()
async updateUser(
@Arg('id', z.string()) id: string,
@Arg('input', CreateUserSchema.partial()) input: Partial<z.infer<typeof CreateUserSchema>>
): Promise<User> {
return await userService.update(id, input);
}
@GQLMutation()
async deleteUser(@Arg('id', z.string()) id: string): Promise<boolean> {
await userService.delete(id);
return true;
}
}
import { GQLSubscription, Arg } from 'veloce-ts';
@Resolver()
export class NotificationResolver {
@GQLSubscription()
async userNotifications(@Arg('userId', z.string()) userId: string): AsyncIterable<Notification> {
// Retornar un iterador asíncrono para notificaciones en tiempo real
return notificationService.getUserNotifications(userId);
}
@GQLSubscription()
async taskUpdates(@Arg('taskId', z.string()) taskId: string): AsyncIterable<TaskUpdate> {
return taskService.getTaskUpdates(taskId);
}
}
DecoradorPropósitoEjemplo
@Resolver()Marca una clase como resolver GraphQL@Resolver()
@GQLQuery()Define una consulta GraphQL@GQLQuery()
@GQLMutation()Define una mutación GraphQL@GQLMutation()
@GQLSubscription()Define una suscripción GraphQL@GQLSubscription()
@Arg(name, schema)Define argumentos GraphQL@Arg('id', z.string())
import { Arg } from 'veloce-ts';
const PaginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(10),
sortBy: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc')
});
const FilterSchema = z.object({
name: z.string().optional(),
email: z.string().email().optional(),
role: z.enum(['admin', 'user', 'guest']).optional()
});
@Resolver()
export class UserResolver {
@GQLQuery()
async users(
@Arg('pagination', PaginationSchema) pagination: z.infer<typeof PaginationSchema>,
@Arg('filters', FilterSchema) filters: z.infer<typeof FilterSchema>
): Promise<{ users: User[]; total: number; page: number }> {
const result = await userService.findWithPagination(pagination, filters);
return {
users: result.users,
total: result.total,
page: pagination.page
};
}
}

Veloce-TS convierte automáticamente esquemas Zod a tipos GraphQL:

// Esquema Zod
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0),
isActive: z.boolean(),
createdAt: z.date(),
tags: z.array(z.string())
});
// Genera automáticamente el tipo GraphQL:
// type User {
// id: String!
// name: String!
// email: String!
// age: Int!
// isActive: Boolean!
// createdAt: DateTime!
// tags: [String!]!
// }
import { GraphQLScalarType } from 'graphql';
const DateScalar = new GraphQLScalarType({
name: 'Date',
description: 'Tipo escalar personalizado de fecha',
serialize: (value: Date) => value.toISOString(),
parseValue: (value: string) => new Date(value),
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
}
});
// Registrar escalar personalizado
app.install('graphql', {
path: '/graphql',
scalars: {
Date: DateScalar
}
});
import { Auth, CurrentUser } from 'veloce-ts';
@Resolver()
export class UserResolver {
@GQLQuery()
@Auth() // Requerir autenticación
async me(@CurrentUser() user: any): Promise<User> {
return user;
}
@GQLMutation()
@Auth()
async updateProfile(
@CurrentUser() currentUser: any,
@Arg('input', UpdateProfileSchema) input: any
): Promise<User> {
// Solo permitir que los usuarios actualicen su propio perfil
if (currentUser.id !== input.userId) {
throw new ForbiddenException('No puedes actualizar otros usuarios');
}
return await userService.update(currentUser.id, input);
}
}
import { NotFoundException, ValidationException } from 'veloce-ts';
@Resolver()
export class UserResolver {
@GQLQuery()
async user(@Arg('id', z.string()) id: string): Promise<User> {
const user = await userService.findById(id);
if (!user) {
throw new NotFoundException(`Usuario con id ${id} no encontrado`);
}
return user;
}
@GQLMutation()
async createUser(@Arg('input', CreateUserSchema) input: any): Promise<User> {
try {
return await userService.create(input);
} catch (error) {
if (error.code === 'DUPLICATE_EMAIL') {
throw new ValidationException('El email ya existe');
}
throw error;
}
}
}
import DataLoader from 'dataloader';
@Resolver()
export class UserResolver {
private userLoader = new DataLoader(async (ids: string[]) => {
const users = await userService.findByIds(ids);
return ids.map(id => users.find(user => user.id === id) || null);
});
@GQLQuery()
async users(): Promise<User[]> {
const userIds = await userService.getAllUserIds();
return await this.userLoader.loadMany(userIds);
}
}
import { Resolver, GQLQuery, GQLMutation, Arg } from 'veloce-ts';
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.date()
});
const CreateUserInput = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']).default('user')
});
const UpdateUserInput = CreateUserInput.partial();
@Resolver()
export class UserResolver {
@GQLQuery()
async users(): Promise<User[]> {
return await userService.findAll();
}
@GQLQuery()
async user(@Arg('id', z.string()) id: string): Promise<User | null> {
return await userService.findById(id);
}
@GQLMutation()
async createUser(@Arg('input', CreateUserInput) input: z.infer<typeof CreateUserInput>): Promise<User> {
return await userService.create(input);
}
@GQLMutation()
async updateUser(
@Arg('id', z.string()) id: string,
@Arg('input', UpdateUserInput) input: z.infer<typeof UpdateUserInput>
): Promise<User> {
return await userService.update(id, input);
}
@GQLMutation()
async deleteUser(@Arg('id', z.string()) id: string): Promise<boolean> {
await userService.delete(id);
return true;
}
}
# Consultar todos los usuarios
query GetUsers {
users {
id
name
email
role
createdAt
}
}
# Consultar usuario específico
query GetUser($id: String!) {
user(id: $id) {
id
name
email
role
}
}
# Mutación para crear usuario
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
role
createdAt
}
}
# Mutación para actualizar usuario
mutation UpdateUser($id: String!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
role
}
}
# Mutación para eliminar usuario
mutation DeleteUser($id: String!) {
deleteUser(id: $id)
}
{
"id": "user-123",
"input": {
"name": "Juan Pérez",
"email": "juan@ejemplo.com",
"role": "user"
}
}
// Bueno: Tipado fuerte con Zod
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150)
});
@GQLMutation()
async createUser(@Arg('input', CreateUserSchema) input: z.infer<typeof CreateUserSchema>) {
// input está completamente tipado y validado
}
@GQLQuery()
async user(@Arg('id', z.string()) id: string): Promise<User> {
try {
const user = await userService.findById(id);
if (!user) {
throw new NotFoundException('Usuario no encontrado');
}
return user;
} catch (error) {
// Registrar error para depuración
console.error('Error al obtener usuario:', error);
throw error;
}
}
// Evitar consultas N+1
@Resolver()
export class PostResolver {
private userLoader = new DataLoader(async (userIds: string[]) => {
return await userService.findByIds(userIds);
});
@GQLQuery()
async posts(): Promise<Post[]> {
const posts = await postService.findAll();
// Cargar usuarios eficientemente
const users = await this.userLoader.loadMany(
posts.map(post => post.authorId)
);
return posts.map(post => ({
...post,
author: users.find(user => user?.id === post.authorId)
}));
}
}