Skip to main content
COSMOS RSC uses Express as its web server. This guide shows you how to customize the server for authentication, middleware, API routes, and more.

Server architecture

The server implementation is in core/server/index.js. It handles:
  • Static file serving from the build directory
  • RSC payload generation and streaming
  • Server action execution
  • Server-side rendering via worker threads
  • Cookie management

Basic server setup

The default server configuration:
const express = require('express');
const { BUILD_DIR } = require('./lib/constants');

const PORT = 8000;
const app = express();

// Serve webpack build artifacts
app.use(express.static(BUILD_DIR));

// Handle all routes
app.get('*splat', requestHandler);
app.post('*splat', requestHandler);

app.listen(PORT, () => {
  logger.info(`Server is running on http://localhost:${PORT}`);
});
The *splat pattern matches all routes, allowing React to handle routing.

Understanding the request handler

The core request handler manages the entire rendering pipeline (core/server/index.js:42):
async function requestHandler(req, res) {
  try {
    // Parse incoming cookies
    const incomingCookies = new Map(
      req.headers.cookie?.split(';').map((cookie) => {
        const [key, ...valueParts] = cookie.split('=');
        return [key.trim(), { value: valueParts.join('=').trim() }];
      }) ?? []
    );

    // Initialize app store
    const appStore = {
      metadata: { renderPhase: 'START' },
      cookies: {
        incoming: incomingCookies,
        outgoing: new Map(),
      },
    };

    runWithAppStore(appStore, async () => {
      // Handle server actions (POST requests)
      if (req.method === 'POST') {
        // ... server action execution
      }

      // Set response cookies
      if (cookies.outgoing.size > 0) {
        res.setHeader('Set-Cookie', getCookieString([...cookies]));
      }

      // Load and render page
      const pagePath = `../../app/pages${req.path}`;
      const Page = require(pagePath).default;
      const tree = createElement(Page, { searchParams: { ...req.query } });

      // Generate RSC payload
      const webpackMap = await getReactClientManifest();
      const rscStream = renderToPipeableStream(payload, webpackMap);

      // Return RSC payload or full HTML
      if (req.headers.accept === 'text/x-component') {
        res.setHeader('Content-Type', 'text/x-component');
        rscStream.pipe(res);
      } else {
        // ... SSR streaming setup
      }
    });
  } catch (error) {
    logger.error(error);
    res.status(500).send('Internal Server Error');
  }
}

Adding custom middleware

You can add Express middleware before the route handlers:

Request logging

const morgan = require('morgan');

app.use(morgan('combined'));
app.use(express.static(BUILD_DIR));

CORS configuration

const cors = require('cors');

app.use(cors({
  origin: 'https://your-domain.com',
  credentials: true,
}));

Request body parsing

Do NOT add body parsing middleware globally. Server actions handle their own request parsing using busboy.
For custom API routes only:
app.use('/api/*', express.json());
app.use('/api/*', express.urlencoded({ extended: true }));

Session management

const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));

Adding API routes

Define custom API routes before the catch-all handler:
app.use(express.static(BUILD_DIR));

// Custom API routes
app.get('/api/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: Date.now() });
});

app.post('/api/webhook', express.json(), async (req, res) => {
  const { event, data } = req.body;
  // Process webhook
  res.json({ received: true });
});

// RSC routes (must come after API routes)
app.get('*splat', requestHandler);
app.post('*splat', requestHandler);

Authentication

Token-based authentication

Add middleware to verify JWT tokens:
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    req.user = user;
    next();
  } catch (error) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// Apply to specific routes
app.get('/api/protected/*', authenticateToken, requestHandler);

Session-based authentication

function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.redirect('/login');
  }
  next();
}

// Apply to protected routes
app.get('/dashboard*', requireAuth, requestHandler);
app.get('/settings*', requireAuth, requestHandler);

Accessing auth in components

Pass authentication data through the request:
async function requestHandler(req, res) {
  // ... existing code
  
  // Pass user data to page component
  const tree = createElement(Page, {
    searchParams: { ...req.query },
    user: req.user, // From authentication middleware
  });
  
  // ... rest of request handler
}

Custom headers

Security headers

const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // Required for RSC
      styleSrc: ["'self'", "'unsafe-inline'"],
    },
  },
}));

Custom response headers

app.use((req, res, next) => {
  res.setHeader('X-Powered-By', 'COSMOS RSC');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  next();
});

Error handling

Custom error pages

Add error handling middleware:
// 404 handler (before error handler)
app.use((req, res, next) => {
  if (!req.route) {
    res.status(404).send('Page not found');
  } else {
    next();
  }
});

// Global error handler (last middleware)
app.use((err, req, res, next) => {
  logger.error(err);
  
  if (process.env.NODE_ENV === 'production') {
    res.status(500).send('Internal Server Error');
  } else {
    res.status(500).send(`<pre>${err.stack}</pre>`);
  }
});

Environment configuration

Port configuration

Use environment variables:
const PORT = process.env.PORT || 8000;
const HOST = process.env.HOST || 'localhost';

app.listen(PORT, HOST, () => {
  logger.info(`Server is running on http://${HOST}:${PORT}`);
});

Environment-specific middleware

if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
  app.use((req, res, next) => {
    logger.info(`${req.method} ${req.path}`);
    next();
  });
}

if (process.env.NODE_ENV === 'production') {
  app.use(compression());
  app.set('trust proxy', 1);
}

Advanced customization

Modifying the Fizz worker

The Fizz worker handles SSR in a separate thread (core/server/lib/fizz-worker.js). You can modify it to:
  • Add custom SSR context
  • Inject additional data into the HTML
  • Customize the bootstrap scripts
const htmlStream = renderToPipeableStream(
  createElement(SSRApp, {
    initialState: { tree },
    rootLayout,
    customContext: { /* your data */ },
  }),
  {
    formState,
    bootstrapScripts: [
      '/client.js',
      '/custom-init.js', // Your custom script
    ],
    bootstrapModules: ['/hydrate.js'],
    onShellReady: () => {
      htmlStream
        .pipe(injectRSCPayload(payloadConsumerRSCStream))
        .pipe(writableStream);
    },
  }
);

Custom static file serving

Serve files from multiple directories:
app.use(express.static(BUILD_DIR));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
app.use('/assets', express.static(path.join(__dirname, 'public')));
With caching:
app.use(express.static(BUILD_DIR, {
  maxAge: process.env.NODE_ENV === 'production' ? '1y' : 0,
  immutable: true,
}));

Favicon handling

The server has special handling for favicons (core/server/index.js:190):
if (req.path === '/favicon.ico') {
  if (fs.existsSync(FAVICON_PATH)) {
    res.sendFile(FAVICON_PATH);
  } else {
    res.status(404).end();
  }
  return;
}
Customize the favicon path in core/server/lib/constants.js.

Deployment considerations

Graceful shutdown

const server = app.listen(PORT, HOST, () => {
  logger.info(`Server is running on http://${HOST}:${PORT}`);
});

process.on('SIGTERM', () => {
  logger.info('SIGTERM signal received: closing HTTP server');
  server.close(() => {
    logger.info('HTTP server closed');
    fizzWorker.terminate();
    process.exit(0);
  });
});

Health checks

app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    uptime: process.uptime(),
    timestamp: Date.now(),
  });
});

app.get('/ready', async (req, res) => {
  try {
    // Check if manifests are loaded
    await getReactClientManifest();
    await getReactSSRManifest();
    res.json({ ready: true });
  } catch (error) {
    res.status(503).json({ ready: false, error: error.message });
  }
});

Proxy configuration

Behind a reverse proxy:
app.set('trust proxy', true);

app.use((req, res, next) => {
  // Access original IP
  const clientIP = req.ip;
  // Access original protocol
  const protocol = req.protocol;
  next();
});

Limitations and considerations

Do not modify the core request handler’s RSC rendering logic unless you understand the implications. Changes can break server actions, streaming, or hydration.

What you can safely customize:

  • Add middleware before the request handler
  • Add custom API routes
  • Modify static file serving
  • Customize error handling
  • Add authentication and authorization

What to avoid:

  • Parsing request bodies globally (breaks server actions)
  • Modifying RSC stream handling
  • Changing worker thread communication
  • Altering manifest loading

Build docs developers (and LLMs) love