Entwickler Portfolio
Einführung
Lange Zeit habe ich das Erstellen einer persönlichen Portfolio-Website hinausgezögert, um meine Fähigkeiten, Erfahrungen und Projekte zu präsentieren. Nach der Erkundung verschiedener Tools entschied ich mich, mit ein paar Freunden zusammenzuarbeiten—einer als Designer und der andere als Frontend-Entwickler—um die Website von Grund auf neu zu bauen. Gemeinsam haben wir den Inhalt und das Design erarbeitet und schließlich beschlossen, das Projekt mit Next.js als Hauptframework umzusetzen.
Technologie und Implementierung
Next.js und Hosting auf Vercel
Nach der Diskussion verschiedener Optionen habe ich mich schnell auf Next.js geeinigt, aufgrund seiner Flexibilität und Leistung. Für das Hosting wählte ich Vercel, das sich nahtlos in Next.js integriert und eine schnelle Bereitstellung und Skalierbarkeit bietet.
Ursprüngliche CMS-Pläne und Integration von Payload CMS
Ursprünglich plante ich, ein externes CMS zu verwenden, aber nach Recherche und Gesprächen mit meinem Frontend-Entwickler-Kollegen entdeckte ich Payload CMS. Dessen nahtlose Integration mit Next.js machte es zur offensichtlichen Wahl. Payload ermöglichte es mir, Inhalte effizient innerhalb meiner Anwendung zu verwalten, ohne auf Drittanbieter-Dienste angewiesen zu sein.
Projektaufbau und Architektur
Begeistert von dem Plan begann ich mit der Arbeit am Projekt und wollte einen Proof of Concept erstellen. Ziel war es, eine saubere Architektur einzurichten, Fehlerbehandlung mithilfe von Effect sicherzustellen und Payload CMS zu integrieren. Auf dem Weg stieß ich auf eine wunderschöne Vorlage von Chronark, die ich beschloss, für meine Portfolio-Website anzupassen. Diese Vorlage bot einen ausgezeichneten Ausgangspunkt, sodass ich mich auf die Anpassung von Inhalt und Design für meine persönliche Marke konzentrieren konnte.
CLEAN-Implementierung
1. Entities Layer: Zentrale Geschäftslogik
Die Entities-Schicht enthält meine zentrale Geschäftslogik. Hier definiere ich meine Domain-Modelle und die zugehörige Logik, wie z.B. benutzerdefinierte Fehlerbehandlung und Datenvalidierung. Lassen Sie uns damit beginnen, zu zeigen, wie benutzerdefinierte Fehler behandelt werden und wie Zod für eine strikte Schema-Validierung von Datenmodellen verwendet wird.
Benutzerdefinierte Fehler
In diesem Fall verwende ich einen benutzerdefinierten Fehler (BlogNotFoundError), der die eingebaute Error-Klasse von JavaScript erweitert. Dieser benutzerdefinierte Fehler ermöglicht es mir, spezifische Fehler im Zusammenhang mit meiner Blog-Funktionalität zu verwalten, z.B. wenn ein Blog nicht gefunden wird.
import { BaseError } from './types';
export class BlogNotFoundError extends Error {
constructor(data: BaseError = {}) {
super(JSON.stringify(data));
this.name = 'BlogNotFoundError';
}
}
Zod-validierte Datenmodelle
export const blogSchema = z.object({
id: z.number(),
title: z.string(),
summary: z.string().optional().nullable(),
slug: z.string(),
author: z.string(),
authorImage: z.object({
url: z.string(),
alt: z.string(),
}).optional().nullable(),
thumbnail: z.object({
url: z.string(),
alt: z.string(),
width: z.number(),
height: z.number(),
}).optional().nullable(),
date: z.string().transform((str) => new Date(str)),
published: z.boolean().optional().nullable(),
type: z.string(),
});
Hier verwende ich Zod, um ein Schema für mein Blog-Entität zu definieren. Das Schema hilft sicherzustellen, dass die Datenmodelle der erwarteten Struktur entsprechen. Ich verwende auch z.infer, um die Typen aus dem Schema abzuleiten:
export type Blog = z.infer<typeof blogSchema>;
export type BlogDto = z.input<typeof blogSchema>;
2. Anwendungsschicht: Anwendungsfälle und Repositories
Die Anwendungsschicht enthält die Anwendungsfälle, die Geschäftsoperationen abwickeln und mit Repositories interagieren, um Daten abzurufen und zu speichern. Hier ist ein Beispiel für die Repository-Schnittstelle und die Anwendungsfall-Implementierung zum Abrufen eines Blogs anhand seines Slugs.
Repository-Schnittstelle
import { BlogWithDetailsDto } from '../../entities/models/blog';
export interface IBlogsRepository {
getBlogBySlug(slug: string): Promise<BlogWithDetailsDto>;
}
Use-Case
export function getBlogBySlugUseCase(slug: string): Effect.Effect<BlogWithDetails, BlogNotFoundError | ZodParseError> {
const repository = getInjection('IBlogsRepository');
const program = Effect.tryPromise({
async try() {
const blog = await repository.getBlogBySlug(slug);
if (!blog) {
throw new BlogNotFoundError();
}
return blog;
},
catch(error: unknown) {
return new BlogNotFoundError({
originalError: error
})
}
});
const parseBlogEffect = (blog: unknown) =>
Effect.try({
try() {
return blogDetailSchema.parse(blog);
},
catch(_error: unknown) {
return new ZodParseError('BlogWithDetails', {
originalError: _error,
data: blog
});
},
});
return program.pipe(Effect.flatMap(parseBlogEffect));
}
Dieser Anwendungsfall ruft den Blog aus dem Repository ab, behandelt Fehler (z.B. Blog nicht gefunden) und stellt sicher, dass die Daten dem erforderlichen Schema mit Zod entsprechen.
3. Infrastrukturschicht: Implementierung von Repositories
Die Infrastrukturschicht enthält die tatsächlichen Implementierungen der Repository-Schnittstellen, die mit externen Systemen oder Datenbanken interagieren.
In diesem Fall verwende ich Payload CMS und seine lokale API, um Blog-Daten abzurufen.
@injectable()
export class PayloadBlogsRepository implements IBlogsRepository {
_getPayload() {
return getPayloadHMR({ config });
}
constructor() {}
async getBlogBySlug(slug: string): Promise<BlogWithDetailsDto> {
const payload = await this._getPayload();
const locale = await getSafeLocale();
const blog = await payload.find({
collection: 'blogs',
locale,
where: {
slug: {
equals: slug
}
},
});
return blog.docs?.[0] as BlogWithDetailsDto;
}
}
Ich verwende Inversify für Dependency Injection. Die Klasse PayloadBlogsRepository implementiert die Schnittstelle IBlogsRepository, sodass ich das Repository bei Bedarf injizieren kann.
4. Schnittstellenadapter-Schicht: Controller und Presenter
Die Schnittstellenadapter-Schicht überbrückt die Lücke zwischen Infrastruktur- und Anwendungsschicht, indem sie Daten für die Verwendung in der Benutzeroberfläche oder API formatiert.
Controller-Beispiel
function presenter(blog: BlogWithDetails) {
return {
...blog,
gallery: staticImages(blog.gallery),
authorImage: staticImage(blog.authorImage)
}
}
export function getBlogBySlugController(slug: string): Effect.Effect<ReturnType<typeof presenter>, BlogNotFoundError | ZodParseError> {
return Effect.map(
getBlogBySlugUseCase(slug),
(blog) => presenter(blog)
);
}
Der Controller hier formatiert die Blog-Daten für das Frontend. Die Funktion getBlogBySlugController verbindet alles, indem sie den Anwendungsfall getBlogBySlugUseCase aufruft, Fehler behandelt und das Ergebnis im passenden Format präsentiert.
Ich kann den Controller nun ausführen und sicherstellen, dass alle Parsing-Fehler erfasst und protokolliert werden.
await Effect.runPromise(
getBlogBySlugController(slug)
.pipe(
Effect.catchAll((error) => {
if (error instanceof ZodParseError) {
console.error('Zod validation error:', error);
} else {
console.error('An unexpected error occurred:', error);
}
return Effect.fail(error); // Re-throw the error after logging
})
)
);
Ein großes Dankeschön an Laza Nikolov für die detaillierte Erklärung zur Implementierung der CLEAN-Architektur in Next.js.
Warum Effect verwenden?
Ich verwende Effect, um Seiteneffekte auf funktionale und zusammensetzbare Weise zu handhaben. Es ermöglicht mir, asynchronen Code und Fehlerbehandlung eleganter zu verwalten, indem es die Ausführung von der Definition von Effekten trennt und so klarere, besser wartbare Logik sicherstellt.
Dependency Injection mit Inversify
Ich verwende Inversify für Dependency Injection, um meine Anwendungsschichten zu entkoppeln. Dies ermöglicht flexibleren und testbareren Code, indem die passenden Abhängigkeiten (wie das Repository) dort injiziert werden, wo sie benötigt werden, ohne diese fest miteinander zu verknüpfen.
Hauptfunktionen
- Next.js-Framework für schnelle, serverseitig gerenderte Inhalte.
- Vercel-Hosting für nahtlose Bereitstellung und Leistung.
- Supabase-Datenbank für skalierbare Datenspeicherung.
- Payload CMS-Integration für Content-Management innerhalb der Next.js-App.
- CLEAN-Architektur-Prinzipien für Wartbarkeit und Skalierbarkeit.
- Effect für robuste Fehlerbehandlung in der gesamten App.
Aktueller Status
Die erste Version meiner Portfolio-Website ist jetzt live. Obwohl sie noch in Arbeit ist und sich weiterentwickeln kann, dient die aktuelle Iteration als solide Grundlage, um meine Erfahrung und Fähigkeiten zu präsentieren. Sie können die Website hier ansehen.
Fazit
Dieses Projekt bot mir die Gelegenheit, endlich eine persönliche Portfolio-Website mit modernen Tools wie Next.js, Vercel und Payload CMS zu erstellen. Die enge Zusammenarbeit mit einem Designer und einem Frontend-Entwickler ermöglichte eine schnelle Entwicklung, und die Integration einer sauberen Architektur stellt sicher, dass die Website im Laufe der Zeit wachsen und sich anpassen kann.
Angewendete Tools/Technologien:
Next.js, React, TypeScript, Vercel, PayloadCMS, Supabase
Besuchen Sie die Seite hier: glappa.dev