Company Logo

Bisht Bytes

How to setup a Nextjs 14 custom server

Published On: 24 Jul 2024
Reading Time: 9 minutes

Overview


How to setup a Nextjs 14 custom server

When developing with Next.js, one of the standout features is the ability to seamlessly integrate with an existing backend or to set up a custom server. While Next.js provides a powerful and flexible server out of the box, there are scenarios where you may need more control over your server setup. This is where a custom Next.js server comes into play.

In this blog, we'll explore what a custom Next.js server is, when you might need one, and how to set it up with TypeScript.

Why Use a Custom Next.js Server?

By default, Next.js handles server-side rendering and API routes using its own server with the next start command. However, there are situations where you might need a custom server:

  1. Custom Routing: If you need advanced routing logic, such as dynamic routes, rewrites, or redirects that aren't possible with Next.js' built-in routing.
  2. Middleware: Adding middleware for things like authentication, logging, or custom headers before handling the request.
  3. Custom Server Patterns: When integrating with other frameworks or libraries that require specific server configurations.
  4. Setting up event listeners: You could instantiate services that needs to start along with the server. This was previously not possible. Custom server are especially useful in such scenarios.

Setting Up a Custom Next.js Server with TypeScript

Let's dive into setting up a custom Next.js server using TypeScript.

Step 1: Initial Setup

First, make sure you have a Next.js project set up. If not, you can create one using:

npx create-next-app my-custom-server
cd my-custom-server

Next, install the necessary dependencies:

npm install typescript ts-node nodemon tsconfig-paths cross-env

Step 2: Configure TypeScript

Add or update your tsconfig.json and create a new tsconfig.server.json for the server-side TypeScript configuration. This allows you to use a seperate typescript configuration for your custom server and keep the typescript config for your frontend independent of it.

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}
// tsconfig.server.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "module": "commonjs",
    "target": "es6",
    "noEmit": false
  },
  "include": ["server/**/*.ts"]
}

Step 3: Create the Custom Server

Create a new file server.ts and set up your custom server logic.

// server.ts
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
 
const dev = process.env.NODE_ENV !== 'production'
const hostname = 'localhost'
const port = 3000
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port })
const handle = app.getRequestHandler()
 
app.prepare().then(() => {
  createServer(async (req, res) => {
    try {
      // Be sure to pass `true` as the second argument to `url.parse`.
      // This tells it to parse the query portion of the URL.
      const parsedUrl = parse(req.url, true)
      const { pathname, query } = parsedUrl
 
      if (pathname === '/a') {
        await app.render(req, res, '/a', query)
      } else if (pathname === '/b') {
        await app.render(req, res, '/b', query)
      } else {
        await handle(req, res, parsedUrl)
      }
    } catch (err) {
      console.error('Error occurred handling', req.url, err)
      res.statusCode = 500
      res.end('internal server error')
    }
  })
    .once('error', (err) => {
      console.error(err)
      process.exit(1)
    })
    .listen(port, () => {
      console.log(`> Ready on http://${hostname}:${port}`)
    })
})

Step 4: Setup Nodemon

Create a nodemon.json configuration file to automatically restart the server when changes are detected. This is to setup dev environment for local development.

// nodemon.json
{
  "watch": ["server.ts"],
  "exec": "ts-node --project tsconfig.server.json -r tsconfig-paths/register src/server.ts",
  "ext": "js ts",
  "env": {
    "DOTENV_CONFIG_PATH": ".env.local",
    "DEBUG": ""
  }
}

Step 5: Update package.json Scripts

Update your package.json to include the necessary scripts for development, building, and starting the custom server.

// package.json
{
  "scripts": {
    "dev": "nodemon",
    "build": "next build && tsc --project tsconfig.server.json",
    "start": "cross-env NODE_ENV=production DEBUG=socket.io* node dist/src/server.js",
    "docker-run": "node dist/src/server.js",
  }
}

Step 6: Running the Custom Server

To start the custom server in development mode, use:

npm run dev

For production, build and start the server:

npm run build
npm start

Pushing the code to production using docker requires you to configure your Dockerfile as well. In the following Dockerfile the most important is:

  • copying the node_modules to the image
  • run the server using CMD ["npm", "run", "docker-run"]
# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN \
  if [ -f yarn.lock ]; then yarn run build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["npm", "run", "docker-run"]

References


Page Views: -