How to setup a Nextjs 14 custom server
Overview
- How to setup a Nextjs 14 custom server
- Why Use a Custom Next.js Server?
- Setting Up a Custom Next.js Server with TypeScript
- Step 1: Initial Setup
- Step 2: Configure TypeScript
- Step 3: Create the Custom Server
- Step 4: Setup Nodemon
- Step 5: Update `package.json` Scripts
- Step 6: Running the Custom Server
- References
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:
- 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.
- Middleware: Adding middleware for things like authentication, logging, or custom headers before handling the request.
- Custom Server Patterns: When integrating with other frameworks or libraries that require specific server configurations.
- 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
Related Articles
CASL Ability Based Http Client to secure NextJS server actions
Explore how to use the AbilityBasedHttpClient class to integrate access control into your API requests using CASL and TypeScript.
08/10/2024
Monorepo alternative to sharing routes for NextJs 14 in 2024
How you can achieve code sharing at page level without setting up a monorepo using Nextjs 14 colocation capability
28/07/2024
The Evolution of Web Development
Isomorphic JavaScript, SSR, Hydration, and Resumability
23/07/2024
Creating npm package for typescript based library in 2024
Create npm package that are compatible with both ESM and CJS
13/07/2024