Como construí un sitio web moderno en 2021

Un vistazo a las estadísticas

Primero, para que te hagas una idea de la escala del proyecto, vamos a ver algunos datos y estadísticas:

Un vistazo a las tecnologías

Estas son las principales tecnologías usadas en el proyecto (no necesariamente en este orden):

  • React: Para el UI
  • Remix: Framework para el enrutado Cliente/Servidor.
  • TypeScript: JavaScript tipado (muy recomendable para cualquier proyecto que requiera mantenimiento)
  • XState: Una herramienta para crear máquinas de estados, haciendo lo complicado realmente simple.
  • Prisma: Un fantástico ORM con soporte para TypeScript.
  • Express: Framework para servidores de Node.
  • Cypress: Framework para tests E2E.
  • Jest: Framework para hacer tests unitarios y de componentes.
  • Testing Library: Utilidades para testar de manera simple interfaces basadas en el DOM.
  • MSW: Herramienta magnífica para mockear peticiones de HTTP en el navegador y en node
  • Tailwind CSS: Utilidad para trabajar con clases consistentes y mantenibles en los estilos.
  • Postcss: Procesador de CSS, usado sobre todo para tailwind y autoprefijar.
  • Reach UI: Un cnojunto de componentes de UI que necesita cualquier aplicación (acordeón, pestañas, modales, etc.).
  • ESBuild: Bundler de JavaScript (usado por Remix y mdx-bundler).
  • mdx-bundler: Herramienta para compilar y bundlig para sitios escritos en MDX(posts y otras páginas simples).
  • Octokit: Librería para hacer que la interacción con GitHub sea más sencilla.
  • Framer Motion: Una gran librería de animaciones de React.
  • Unified: sistema que compila Markdown/HTML/transformer.
  • Postgres: Base de datos SQL a prueba de bombas.
  • Redis: In-memory database–key/value store.
  • Fly.io: Superplataforma para el hosting.
  • GitHub Actions: Servicio de pipelines.
  • Honeycomb: Servicio de telemetría.
  • Sentry: Servicio para reportar errores.
  • Cloudinary: Fantástico hosting para alojar y transformar imagenes.
  • Fathom: Servicio de analíticas centrado en la privacidad y la ética.
  • Metronome: Servicio de métircas de Remix.

Un vistazo a la arquitectura

Pipeline de despliegue

Action de Github: 🥬Refresh content

Esta primera action de GitHub la he llamado “🥬Refresh content” y su intención era referscar cualquier contenido que hubiera cambiado. Antes de describir lo que hace, dejame explicarte como lo solucioné. La verisón previa de kentcdodds.com estaba escrita en Gatsby y debido a su naturaleza de SSG (generador de sitios estáticos), cada vez que quería hacer un cambio, tenía que hacer un build de todo el sitio (que podía tardar entre 10 y 25 minutos).

Action de GitHub: 🚀 Despliegue

La segunda action despliega el sitio. Primero, determina los cambios que se puede desplegar. Si solo hay cambios de contenido, entonces no hay razón para hacer un despliegue, sino que basta con hacer un refresh. La gran mayoría de los commits del sitio antiguo, eran solamente de contenido, por lo que esto ayuda a salvar árboles.🌲🌴🌳

  • ⬣ ESLint: Permite evitar fallos simples en el proyecto.
  • ʦ TypeScript: comprobación de tipos para los errores del proyecto
  • 🃏 Jest: Ejecutar los test unitarios y de componentes.
  • ⚫️ Cypress: Ejecutar los tests E2E.
  • 🐳 Build: Ejecutar las imagenes del docker.

Conectividad de la Base de Datos

Fly Request Replays

Desarrollo local con MSW

Cuando estoy desarrollando localmente, tengo mis bases de datos de postgres y redis ejecutando en un contenedor de docker con un simple docker-compose.yml. Pero también interactúo con muchas APIs de terceros. Ahora mismo (Septiembre 2021), mi contiene todas estas APIs de terceros:

  1. api.github.com
  2. oembed.com
  3. api.twitter.com
  4. api.tito.io
  5. api.transistor.fm
  6. s3.amazonaws.com
  7. discord.com/api
  8. api.convertkit.com
  9. api.simplecast.com
  10. api.mailgun.net
  11. res.cloudinary.com
  12. www.gravatar.com/avatar
  13. verifier.meetchopra.com
node .
node --require ./mocks .

MSW es un impulso de confianza y productividad enorme para mí

Cacheando con Redis/LRU

Como se ha descrito anteriormente en el diagrama de la arquitectura, tengo alojada la cache de redis en Fly.io. Es fenomenal. Pero la he construido haciendo una pequeña abstracción para interactuar con con redis para tener algunas características especiales que mercen la pena.

type CacheMetadata = {
createdTime: number
maxAge: number | null
}
// it's the value/null/undefined or a promise that resolves to that
type VNUP<Value> = Value | null | undefined | Promise<Value | null | undefined>

async function cachified<
Value,
Cache extends {
name: string
get: (key: string) => VNUP<{
metadata: CacheMetadata
value: Value
}>
set: (
key: string,
value: {
metadata: CacheMetadata
value: Value
},
) => unknown | Promise<unknown>
del: (key: string) => unknown | Promise<unknown>
},
>(options: {
key: string
cache: Cache
getFreshValue: () => Promise<Value>
checkValue?: (value: Value) => boolean
forceFresh?: boolean
request?: Request
fallbackToCache?: boolean
timings?: Timings
timingType?: string
maxAge?: number
}): Promise<Value> {
// do the stuff...
}

// here's an example of the cachified credits.yml that powers the /credits page:
async function getPeople({
request,
forceFresh,
}: {
request?: Request
forceFresh?: boolean
}) {
const allPeople = await cachified({
cache: redisCache,
key: 'content:data:credits.yml',
request,
forceFresh,
maxAge: 1000 * 60 * 60 * 24 * 30,
getFreshValue: async () => {
const creditsString = await downloadFile('content/data/credits.yml')
const rawCredits = YAML.parse(creditsString)
if (!Array.isArray(rawCredits)) {
console.error('Credits is not an array', rawCredits)
throw new Error('Credits is not an array.')
}

return rawCredits.map(mapPerson).filter(typedBoolean)
},
checkValue: (value: unknown) => Array.isArray(value),
})
return allPeople
}
  • Value se refiere al valor que deebría ser almacenado/devuelto por la cache.
  • Cache es simplemente un objeto que tiene un name (para loguear), y get, set, y del.
  • CacheMetadata es información que puede ser guardada a lo largo de todo, el valor determinando cuando debería ser refrescado.
  • key el identificador del valor.
  • cache la cache a utilizar.
  • getFreshValue la funciçon que realmente devuelve el valor. Esto es lo que deberíamos ejecutar siempre que un valor no esté en la cache. Una vez tengamos el valor fresco, el valor se almacena en cache con la key .
  • checkValue es una función que verifica que el valor devuelto de cache/ getFreshValue es correcto. Es posible que despliegue un cambio a getFreshValue que cambie el Value y si el valor en la cache no es correcto entonces queremos forzar getFreshValue a ser llamado y evitar errores de ejecución. También usamos esto para asegurarnos de que lo que pillamos de getFreshValue es correcto y que si no lo es lanzará un mensaje de error.
  • forceFresh es justo lo que piensas. Evita mirar la cache y llama directamente a getFreshValue incluso si el valor todavía no ha expirado
  • request se usa para determinar el valor por defecto de forceFresh . Si la llamada tiene el parametro ?fresh y el usuario tiene el rol de ADMIN (solamente yo) entonces el forceFresh tendrá el valor true. Esto me permite refrescar la cache manualmente para todos los recursos en cualquier página- No necesito hacer esto muy a menudo.
  • fallbackToCache si hemos intentado un forceFresh (por lo que nos hemos saltado la cache) y el obtener los valores frescos falla, entonces tal vez tengamos que hacer un fallback a la cache en lugar de enviar un error. Esto controla esta casuística y por defecto es true .
  • timings y timingsType son usados para otra utilidad que tengo para trackear cuanto tardan las cosas en ser enviadas de nuevo en el header deServer-Timing (útil para identificar cuellos de botella).
  • maxAge controla cuanto tengo que mantener cacheado un valor antes de intentar refrescarlo automáticamente.

Optimizacion de imágenes con Cloudinary

Vale amigos… Cloudinary es increíble. Todas las imagenes del sitio están hosteadas en cloudinary y después son enviadas a tu navegado en el tamaño perfecto y el formato de tu dispositivo. Me ha llevado trabajo (y mucho dinero… cloudinary no es barato) hacer que la magia ocurra, pero ahorra una tonelada de ancho de banda y hace que la carga de imagenes sea más rápida.

Cloudinary me voló la mente y soy feliz por pagar por todo lo que me da

Compilación de MDX con mdx-bundler

He estado usando MDX para escribir los post desde que me fui de Medium. Me encanta poder tener trocitos que interactúan en medio de mi blog sin tener que meterlos a duras penas en el código de mi web.

Interacción con la base de datos con Prisma

Hablemos de Prisma. No soy una persona de bases de datos… para nada. Todo el tema del backend está completamente fuera de mi alcance. Lo gracioso aquí es que Remix hace que el backend sea mucho más cercano que todo el trabajo que estuve haciendo con el backend en los meses anteriores 😆. Y no puedo estar más contento de como Prisma trabaja con las bases de datos. No solo haciendo queries de Postgres, sino tambien con las migraciones. Es realmente interesante lo sencillo que lo hace Prisma. Así que hablemos de algunas cosas.

Migraciones

Con prisma, describes un modelo de base de datos con un fichero schema.prisma. Entonces le dices a Prisma que use eso para actualizar la base de datos para reflejar tu esquema. Si alguna vez necesitas cambiar tu esquema, puede lanzar el comando prisma migrate dev --name <descriptive-name> y prisma generará las queries de SQL necesarias para hacer todas las actualizaciones que necesite.

Typescript

El fichero schema.prisma puede ser utilizado tambien para generar tipos para tu base de datos y aquí es donde la cosas se pone interesante. Este es un rápido ejemplo de una query.

const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
},
})
// This is users type. To be clear, I don't have to write this
// myself, the call above returns this type automatically:
const users: Array<{
id: string
email: string
firstName: string
}>
const users = await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
team: true, // <-- just add the field I want
},
})
const users: Array<{
id: string
email: string
firstName: string
team: Team
}>
const users = await prismaRead.user.findMany({
select: {
id: true,
email: true,
firstName: true,
team: true,
postReads: {
select: {
postSlug: true,
},
},
},
})
const users: Array<{
firstName: string
email: string
id: string
team: Team
postReads: Array<{
postSlug: string
}>
}>
type LoaderData = Await<ReturnType<typeof getLoaderData>>

async function getLoaderData() {
const users = await prismaRead.user.findMany({
select: {
id: true,
email: true,
firstName: true,
team: true,
postReads: {
select: {
postSlug: true,
},
},
},
})
return {users}
}

export const loader: LoaderFunction = async ({request}) => {
return json(await getLoaderData())
}

export default function UsersPage() {
const data = useLoaderData<LoaderData>()
return (
<div>
<h1>Users</h1>
<ul>
{/* all this auto-completes and type checks!! */}
{data.users.map(user => (
<li key={user.id}>
<div>{user.firstName}</div>
</li>
))}
</ul>
</div>
)
}

Prisma ha hecho que yo, un desarrollador frontent, se sienta con la capacidad de trabajar directamente con una base de datos.

Autenticación con links mágicos

Hace un tiempo escribí unas palabras que me estoy comiendo…

  • Obtener el ID de sesión de la cookie de sesión.
  • Obtener el ID de usuario de la sesión.
  • Obtener el usuario.
  • Actualizar el tiempo de expiración para que los usuarios activos no tengan que reautenticarse.
  • Si cualquier cosa falla, limpiamos y redirigimos.

Remix

  1. La encillez con la que se comunican servidor y cliente. Pedir demasiados datos ya no serña un problema porque ahora es muy fácil para mí filtrar lo que quiero en código y tener exactamente lo que necesito en el cliente. Por ello, no hay necesidad de tener un backend de graphql complicado y una librería de cliente para lidiar con ello (puede seguir usando graphql con remix sin ningún problema). Este posts es enorme y voy a escribir muchos más sobre esto en los próximos meses.
  2. El desempeño que tengo con el uso de la plataforma web de Remix. Esto tambien es algo enorme que va a requerir de gran cantidad de blogs para explicarlo.
  3. La posibilidad de tener css para una ruta específica y saber que no va a haber ningún conflicto con el css de cualquier otra ruta. 👋 adiós CSS en JS.
  4. El hecho de que no tenga que pensar en la cache del servidor por que remix se encarga de ello por mí (incluyendo las mutaciones). Todos mis componente pueden asumir que los datos están listos para ser enviados. Gestionar excepciones/errores es muy declarativo. Y remix no implementa su propia cache, pero en su lugar, ayuda a l navegador a hacer las cosas super rápido, incluso después del reload.
  5. Cero preocupaciones por un componente tipo Layout de otros frameworks y los beneficios que me ofrece desde la perspectiva de carga de datos. De nuevo, esto necesitará un post.
  6. No worrying about a Layout component like with other frameworks and the benefits that offers me from a data-loading perspective. Again, this will require a blog post.

Conclusión

No puedo contarte lo mucho que he aprendido consturyendo este sitio. Ha sido realmente divertido y estoy emocionado de plasmar todos mis conociemientos en posts y talleres para enseñarte como he hecho esto para que lo hagas tu también. De hecho, ya tengo preparado lgún taller para que te unas. Reserva ya tus tickets. Estoy desenado verte ahí. Cuídate y sigue trabajando duro.

--

--

Desarrollador front end en Biko2, Navarra

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Diego Razquin

Diego Razquin

Desarrollador front end en Biko2, Navarra