DEV Community

Cover image for Creating an SSR Application on Next.js 14
u4aew
u4aew

Posted on

Creating an SSR Application on Next.js 14

Introduction

Hello! In this article, we will explore how to create a simple web application with server-side rendering (SSR) and why it might be useful.

Server-side rendering (SSR) allows us to deliver pages to users more quickly, improving performance metrics and user experience. This is especially beneficial when users have budget smartphones or slow internet connections, as all heavy requests and computations are handled on the server.

For SSR, we will use Next.js 14. Next.js is a popular framework that offers many useful features out of the box:

  • Server-side and static rendering
  • Automatic code optimization
  • Built-in TypeScript support
  • Smart routing
  • API routes for building a backend
  • Version 14+ introduces new capabilities, including enhanced performance, a new file-system-based router, and improved support for server components.

Project Idea
For our example, we will develop an MVP of a financial exchange with basic functionality. Here’s what our application will be able to do:

  • Display a stock catalog with pagination
  • Load additional stocks via a "Show More" button
  • Provide detailed information about each stock on a separate page
  • Show the latest price and percentage change for each stock
  • Display a price change chart for each stock
  • Be SEO-optimized
  • Be server-rendered for improved performance

Implementation

In Next.js, there is a concept of server and client components. Let's explore their differences:

Server Components are processed on the server. They perform API requests, fetch data, and generate HTML markup, which the browser then renders. Advantages include:

  • Reduced JavaScript sent to the client
  • Faster initial page load
  • Direct access to server resources (e.g., databases)
  • Improved SEO optimization

Client Components function like regular React components in the browser. They can:

  • Interact with browser APIs
  • Handle user events
  • Manage local state
  • Use React lifecycle hooks

Here's how we will divide the components of our application:

layout

Server Components (Red):

  1. Header: Static, does not require interactivity
  2. Stock List: Data is loaded on the server
  3. Pagination: Logic is handled on the server for SEO
  4. Stock Details: Data fetched from the server

Client Components (Green):

  1. "Load More" Button: Requires click handling and scroll position preservation
  2. Price Change Chart: Interactive, uses browser APIs for rendering
  3. This separation allows us to leverage the strengths of both server and client components effectively.

Working with the API

To handle stock market data, we will use a public API, but we'll create a BFF (Backend for Frontend) for it. The BFF will allow us to:

  • Retrieve only the data necessary for our application
  • Transform data into a format convenient for the frontend
  • Combine data from multiple sources
  • Cache frequently requested information

This approach will optimize data handling and improve performance.

Server Components

Here's an example of a server component: the stock information page.

import React from 'react';
import { Metadata } from 'next';
import { BASE_URL } from '@/config';
import { serviceStocks } from '@/services';
import { StockIntro } from '@/components/StockIntro';
import { BuyStock } from '@/components/BuyStock';
import Candles from '@/components/Candles/Candles';
import styles from './styles.module.scss';

type Props = {
  params: { slug: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { data: dataIntro } =
    (await serviceStocks.getByTicker(params.slug)) || {};

  if (!dataIntro) {
    return {
      title: 'Stock not found',
    };
  }

  const metaDescription = `Information about stock ${dataIntro.name} (${dataIntro.ticker}). Sector: ${dataIntro.sector}, Country: ${dataIntro.countryOfRisk}`;
  const keywords = `${dataIntro.name}, ${dataIntro.ticker}, ${dataIntro.sector}, ${dataIntro.countryOfRiskName}, stock, invest`;
  const canonicalUrl = `${BASE_URL}/${params.slug}`;

  return {
    title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
    description: metaDescription,
    keywords: keywords,
    openGraph: {
      title: `${dataIntro.name} (${dataIntro.ticker}) - Information about stock`,
      description: metaDescription,
      type: 'website',
    },
    alternates: {
      canonical: canonicalUrl,
    },
  };
}

const PageStock = async ({ params }: Props) => {
  try {
    const [stockData, lastPriceData, candlesData] = await Promise.all([
      serviceStocks.getByTicker(params.slug),
      serviceStocks.getLastPriceByTicker(params.slug),
      serviceStocks.getCandlesByTicker(params.slug),
    ]);

    return (
      <div className={styles.page}>
        <div className={styles.main}>
          <div className={styles.intro}>
            {stockData ? (
              <StockIntro data={stockData.data} />
            ) : (
              <span>Not data</span>
            )}
          </div>
          {candlesData ? (
            <Candles
              currency={stockData?.data?.currency}
              data={candlesData.data}
            />
          ) : (
            <span>Not data</span>
          )}
        </div>
        <div className={styles.side}>
          {lastPriceData && candlesData ? (
            <BuyStock
              candlesData={candlesData.data}
              currency={stockData?.data?.currency}
              data={lastPriceData.data}
            />
          ) : (
            <span>Not data</span>
          )}
        </div>
      </div>
    );
  } catch (error) {
    console.error('Error loading stock data:', error);
    return <div>Error loading stock data</div>;
  }
};

export default PageStock;
Enter fullscreen mode Exit fullscreen mode

Client Components

Here's an example of a client component for the price change chart:

'use client';

import React from 'react';
import dynamic from 'next/dynamic';
import styles from './styles.module.scss';
const Chart = dynamic(() => import('./Chart/Chart'), { ssr: false });

const Candles = ({ data, currency }: { data: any; currency?: string }) => {
  return (
    <div className={styles.chart}>
      <Chart data={data} currency={currency} />
    </div>
  );
};

export default Candles;
Enter fullscreen mode Exit fullscreen mode
import React, { FC } from 'react';
// @ts-ignore
import CanvasJSReact from '@canvasjs/react-charts';
import { ICandle } from '@/typing';
const CanvasJSChart = CanvasJSReact.CanvasJSChart;

interface ChartProps {
  data: {
    candles: ICandle[];
  };
  currency?: string;
}

const Chart: FC<ChartProps> = ({ data, currency }) => {
  const dataPoints = data.candles.map((candle) => ({
    x: new Date(candle.time),
    y: [
      parseFloat(`${candle.open.units}.${candle.open.nano}`),
      parseFloat(`${candle.high.units}.${candle.high.nano}`),
      parseFloat(`${candle.low.units}.${candle.low.nano}`),
      parseFloat(`${candle.close.units}.${candle.close.nano}`),
    ],
  }));

  const options = {
    theme: 'light2',
    axisX: {
      valueFormatString: 'DD MMM',
    },
    axisY: {
      prefix: currency,
    },
    data: [
      {
        type: 'candlestick',
        yValueFormatString: `${currency} ###0.00`,
        xValueFormatString: 'DD MMM YYYY',
        dataPoints: dataPoints,
      },
    ],
  };

  return <CanvasJSChart options={options} />;
};

export default Chart;
Enter fullscreen mode Exit fullscreen mode

For a more detailed look at the code, you can visit the GitHub repository.

SEO Optimization

import { Open_Sans } from 'next/font/google';
import { BASE_URL } from '@/config';
import { Footer } from '@/components/Footer/Footer';
import { Header } from '@/components/Header';
import { Metadata } from 'next';
import type { Viewport } from 'next';
import styles from './layout.module.scss';
import './globals.css';

const roboto = Open_Sans({
  weight: '400',
  subsets: ['latin'],
});

const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  return (
    <html lang="en" className={roboto.className}>
      <body className={styles.layout}>
        <header className={styles.header}>
          <Header />
        </header>
        <main className={styles.main}>
          <div className={styles.content}>{children}</div>
        </main>
        <footer className={styles.footer}>
          <Footer />
        </footer>
      </body>
    </html>
  );
};

export default RootLayout;

export const viewport: Viewport = {
  colorScheme: 'light dark',
  themeColor: '#2196f3',
};

export const metadata: Metadata = {
  title: {
    default: 'Financial Exchange Platform',
    template: '%s | Financial Exchange Platform',
  },
  description:
    'A leading financial exchange platform for trading and investment.',
  applicationName: 'Financial Exchange Platform',
  authors: [
    {
      name: 'Financial Exchange Team',
      url: BASE_URL,
    },
  ],
  metadataBase: new URL(BASE_URL),
  generator: 'Next.js',
  keywords: ['financial', 'exchange', 'trading', 'investment', 'platform'],
  referrer: 'origin',
  creator: 'Financial Exchange Team',
  publisher: 'Financial Exchange Inc.',
  robots: { index: true, follow: true },
  alternates: {
    canonical: BASE_URL,
  },
  icons: {
    icon: '/favicon.ico',
    apple: '/apple-touch-icon.png',
  },
  manifest: '/manifest.webmanifest',
  openGraph: {
    title: 'Financial Exchange Platform',
    description:
      'A leading financial exchange platform for trading and investment.',
    type: 'website',
    url: BASE_URL,
    siteName: 'Financial Exchange Platform',
    images: [
      {
        url: '/images/og-image.jpg',
        width: 800,
        height: 600,
        alt: 'Financial Exchange Platform',
      },
    ],
  },
  appleWebApp: {
    capable: true,
    title: 'Financial Exchange Platform',
    statusBarStyle: 'black-translucent',
  },
  formatDetection: {
    telephone: false,
  },
  abstract: 'A leading financial exchange platform for trading and investment.',
  category: 'Finance',
  classification: 'Financial Services',
  other: {
    'msapplication-TileColor': '#0a74da',
  },
};

Enter fullscreen mode Exit fullscreen mode

Application Deployment

To automate the deployment of our application, we will use GitHub Actions and Docker Hub. This setup will allow us to automatically build and publish Docker images with each push to the main branch of the repository.

name: Docker

on:
  push:
    branches: [ main ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: u4aew/next:latest
Enter fullscreen mode Exit fullscreen mode
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

EXPOSE 6060

CMD ["npm", "start"]
Enter fullscreen mode Exit fullscreen mode

The minimal basic functionality of our application is ready.

Test environment

In the future, to enhance functionality, we will add:

  • WebSocket for real-time price updates
  • Filters and sorting for the stock catalog
  • User authentication
  • A personal dashboard to track selected stocks

Thank you for your attention, and I hope this article was helpful!

Top comments (0)