Skip to main content
Jowy Portfolio follows a modern, service-oriented architecture built on Astro’s static site generation capabilities. The application is organized into clear layers that separate concerns and promote maintainability.

Architectural Principles

The project follows these core architectural principles:
All external API interactions are abstracted into dedicated service modules located in src/server/services/. Each service handles authentication, data fetching, and error handling independently.
Reusable Astro components are organized in src/components/ with clear responsibilities. Each component encapsulates its own styles and behavior.
TypeScript types are defined in src/types/ with dedicated subdirectories for each external service (Spotify, SoundCloud, YouTube).
Multi-language support is built-in using Astro’s i18n routing with dictionaries for each locale.

Core Layers

Presentation Layer

The presentation layer consists of:
  • Pages (src/pages/[...locale]/): Route handlers using Astro’s file-based routing
  • Layouts (src/layouts/): Shared page templates
    • BaseLayout.astro: Core HTML structure, SEO, analytics
    • PageLayout.astro: Content page wrapper
  • Components (src/components/): Reusable UI elements

Service Layer

The service layer (src/server/services/) provides a clean API for external integrations:
// src/server/services/soundcloud/index.ts
export const soundCloudApi = {
  tracks: {
    getFromUser: getUserTracks,
  },
  users: {
    getInfo: getUserInfo,
  },
};
Each service directory contains:
  • auth.ts: Authentication and token management
  • Specific resource handlers (e.g., tracks.ts, users.ts)
  • index.ts: Public API exports
Available Services:

Spotify

Authentication and top tracks fetchingsrc/server/services/spotify/

SoundCloud

User info and track retrievalsrc/server/services/soundcloud/

YouTube

Playlist and video datasrc/server/services/youtube/

Data Layer

Base Fetcher

All HTTP requests use a centralized fetcher with error handling:
// src/lib/BaseFetcher.ts
export async function baseFetcher<T>(
  url: string,
  options: FetcherOptions = {}
): Promise<T> {
  try {
    const response = await fetch(url, { ...options });
    
    if (response.ok) {
      const contentType = response.headers.get("content-type");
      if (contentType && contentType.includes("application/json")) {
        return (await response.json()) as T;
      }
      return null as T;
    }
    
    // Error handling...
    throw new ApiError(errorMessage, response.status);
  } catch (error) {
    if (error instanceof ApiError) throw error;
    throw new NetworkError();
  }
}
Key Features:
  • Generic type support
  • Automatic JSON parsing
  • Structured error handling (ApiError, NetworkError)
  • Centralized request configuration

Caching Strategy

Server-side in-memory cache reduces API calls:
// src/server/services/cache.ts
const apiCache = new Map<string, CachedItem<any>>();

export function getFromCache<T>(key: string): T | null {
  const cached = apiCache.get(key);
  if (cached && cached.expiresAt > Date.now() / 1000) {
    return cached.data as T;
  }
  return null;
}

export function setInCache<T>(
  key: string,
  data: T,
  durationSeconds: number
): void {
  const expiresAt = Date.now() / 1000 + durationSeconds;
  apiCache.set(key, { data, expiresAt });
}
Usage Example:
// src/server/services/spotify/auth.ts
export async function getSpotifyToken(): Promise<string> {
  // 1. Check cache first
  const cachedToken = getFromCache<string>(TOKEN_CACHE_KEY);
  if (cachedToken) return cachedToken;
  
  // 2. Fetch new token if needed
  const res = await baseFetcher<SpotifyToken>(SPOTIFY_TOKEN_URL, {
    headers,
    method: "POST",
    body,
  });
  
  // 3. Cache with 60s buffer before expiration
  setInCache(TOKEN_CACHE_KEY, res.access_token, res.expires_in - 60);
  
  return res.access_token;
}
Tokens are cached with a 60-second buffer before their actual expiration time to prevent race conditions.

Design Patterns

Service Facade Pattern

Each external API is wrapped in a facade that provides a clean, unified interface:
// Consumer code doesn't need to know about authentication
import { soundCloudApi } from '@/server/services/soundcloud';

const tracks = await soundCloudApi.tracks.getFromUser(userId);

Error Handling

Custom error types provide specific error context:
// src/lib/ErrorTypes.ts
export class ApiError extends Error {
  constructor(message: string, public statusCode: number) {
    super(message);
    this.name = "ApiError";
  }
}

export class NetworkError extends Error {
  constructor() {
    super("Network error occurred");
    this.name = "NetworkError";
  }
}

Utility Functions

Helper functions are organized by concern:
  • i18n utilities (src/utils/i18n.ts): Dictionary loading and locale handling
  • Animations (src/utils/animations.ts): Reusable animation helpers
  • Data transformers (src/utils/getSoundCloudTracksLinks.ts, getYoutubeVideosInfo.ts): API response processing

Static Assets

Assets are organized by feature in src/assets/:
src/assets/
├── dj/           # DJ-related images
├── photo/        # General photography
├── producer/     # Producer-related media
├── rrss/         # Social network icons
└── theme/        # Theme-related assets

Configuration Files

// astro.config.mjs
export default defineConfig({
  site: "https://jowy-portfolio.vercel.app/",
  vite: {
    plugins: [tailwindcss()],
  },
  integrations: [sitemap(), compress()],
  i18n: {
    defaultLocale: defaultLang,
    locales: locales,
    routing: {
      prefixDefaultLocale: false,
      redirectToDefaultLocale: true,
    },
  },
});

Best Practices

Server-Only Code: Services in src/server/ are designed to run only during build time. They should never be imported into client-side code.
Path Aliases: Use the @/ alias for imports instead of relative paths:
import { baseFetcher } from '@/lib/BaseFetcher';

Next Steps

Project Structure

Explore the complete directory structure

Routing System

Learn about Astro’s routing and i18n

Build docs developers (and LLMs) love