Esse projeto foi criado com o objetivo de estudar e demonstrar diferentes padroes de design.
O Healthcare é uma plataforma de agendamento de consultas médicas: o paciente se cadastra, preenche um formulário detalhado e marca consulta com um médico. Do outro lado, uma área de admin onde as consultas são confirmadas, agendadas ou canceladas. Construí com Next.js 15 + React para dominar server actions e formulários densos, com validação em cada campo.
Formulário de saúde tem muito campo com regra específica. Usei react-hook-form com Zod para validar, e o schema é a fonte única de verdade. Tipo seguro e mensagem de erro no mesmo lugar:
export const UserFormValidation = z.object({
name: z.string()
.min(2, "Name must be at least 2 characters")
.max(50, "Name must be at most 50 characters"),
email: z.string().email("Invalid email address"),
phone: z.string()
.refine((phone) => /^\+\d{10,15}$/.test(phone), "Invalid phone number"),
});
O que fez diferença foram os refine. O telefone não é "string", é uma string que bate num regex de formato internacional. E o consentimento de tratamento precisa ser true:
treatmentConsent: z.boolean()
.default(false)
.refine((value) => value === true, {
message: "You must consent to treatment in order to proceed",
}),
Num app de saúde você não deixa ninguém avançar sem consentimento explícito. A regra fica declarada no schema, não escondida num if de componente.
Usei Appwrite como banco/BaaS e falo com ele por server actions do Next.js, funções marcadas com "use server" que rodam só no servidor. Criar uma consulta:
"use server";
export const createAppointment = async (data: CreateAppointmentParams) => {
try {
const newAppointment = await databases.createDocument(
DATABASE_ID!,
APPOINTMENT_COLLECTION_ID!,
ID.unique(),
data
);
return parseStringify(newAppointment);
} catch (error) {
console.error(error);
}
};
Sem camada de API REST no meio: o componente chama a função, ela roda no servidor com as credenciais do Appwrite que nunca vazam pro cliente. Menos código de transporte, menos ponto de falha.
A tela de admin precisa de números. Em vez de três queries para contar agendadas, pendentes e canceladas, busco tudo uma vez e agrego em memória:
const totalByStatus = { scheduled: 0, pending: 0, cancelled: 0 };
appointments.documents.forEach((appointment) => {
totalByStatus[appointment.status] += 1;
});
Ao atualizar uma consulta, chamo revalidatePath("/admin") para invalidar o cache do Next e a lista refletir a mudança na hora. Os dados aparecem numa tabela montada com @tanstack/react-table.
Coloquei o Sentry desde cedo. App de agendamento que quebra sem aviso é o pior tipo: o paciente acha que marcou consulta e não marcou. Com o Sentry eu vejo erro de validação, de integração com o Appwrite e de performance acontecendo em produção, em vez de depender de print do usuário.
A interface usa shadcn/ui em cima do Radix e Tailwind, e o deploy é na Vercel.
Deixei autenticação de verdade para depois e me arrependi. O controle de acesso entre paciente e admin fica muito mais natural quando a identidade do usuário está disponível na stack inteira desde o começo. Dá para amarrar cada consulta ao dono e proteger a área de admin sem gambiarra. Implementar isso depois é sempre mais chato do que já começar com ela.