How to Add ExpressJS Server to NextJS Application

Step-by-step instructions to add a simple ExpressJS server to an existing NEXTJS + Typescript application.

Landy
5 min readDec 11, 2020

Requirements

  • ExpressJS
  • Nodemon: watches for file changes and restarts the application
  • ts-node: Allows Typescript to run in a NodeJS environment
  • DotENV: Reads environment variables from .env files
  • Add /server to your src directory

Configuration

Create a tsconfig.server.json file in the application's root directory. We’ll be pointing the nodemon configuration to this file. Extend the tsconfig.server.json file to use your original tsconfig.json and include the path to your server directory, as shown in the code sample below. You’ll also need to add some compiler options to ensure your project compiles to the right directory, using the correct module.

// tsconfig.server.json{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist",
"noEmit": false,
"sourceMap": true
},
"include": ["src/server"]
}

If you’re using aliases, make sure to update your originaltsconfig to include a server alias if needed.

Next, create a nodemon.json file in the application's root directory. This configuration will specify which files to watch, ignore, and a path to the server.

// nodemon.json
{
"verbose": true,
"ignore": ["node_modules", ".next"],
"watch": ["server/**/*"],
"ext": "ts json",
"exec": "ts-node --project tsconfig.server.json -r tsconfig-paths/register -r dotenv/config --files src/server/index.ts"
}

Next, we need to configure the package.json so it starts the server. Change the dev command from next dev to nodemon like shown below:

"scripts": {
"dev": nodemon
. . .
}

Lastly, create a .env file in the projects route directory (if you don’t already have one). We’ll use this file to store configuration for your server. Within the file specify these variables to start.

NODE_ENV=dev
SERVER_DOMAIN=localhost
API_PATH=/api/
SERVER_PORT=3000

Express Server Setup

Before we get started, add a subdirectory in your /server called /routes. We’ll use this directory to store all the sub-routes to your server.

In the /server create an index.ts file. I’ll run through each part of the file with you, so don’t worry!

In the index.ts file, import the express, dotenv, and nextpackages, as well as contents of ./routes (We’ll add the contents a bit later)

import next from 'next' 
import express, { Request, Response } from 'express'
import dotenv from 'dotenv'
import routes from './routes/'

Next, we fetch the data from our .env file. Thanks to dotenv we can easily import and configure the environment variables.

dotenv.config()
const dev = process.env.NODE_ENV !== 'production
const domain = process.env.SERVER_DOMAIN
const apiPath = process.env.API_PATH
const port = (process.env.SERVER_PORT) as number

Storing configuration in .env files allows for easy configuration in different environments. You can create different .env files for different environments, such as development, staging, and production.

Next, create an instance of the next library and pass in the dev variable we defined earlier. next expects an object with a field called dev , this field determines which environment to launch the server.

const next = next({ dev }) 
const handle = next.getRequestHandler()

We also get a request handler via getRequestHandler. We will use this to parse HTTP requests to our server.

Using the next instance, call the prepare method, which returns a promise, and pipe through the then hook.

next.prepare().then(() => {
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(apiPath, routes) app.all('*', (req: Request, res: Response) => {
return handle(req, res)
})
app.listen(port, (err?: any) => {
if (err) throw err
console.log(`> Ready on localhost:${port} - env
${process.env.NODE_ENV}`)
})
})

Within the then callback, instantiate an express instance. We’ll use this instance to configure our routes and start the server.

const app = express()

Next, we’ll define how to handle requests.

app.use(express.json())   
app.use(express.urlencoded({ extended: true }))

These are express middleware functions, and they’re entirely optional for the time being. express.json() parses middleware requests with JSON payloads and express.urlencoded parses requests with URL-encoded payloads.

I strongly advise you to check out the documentation.

app.use(apiPath, routes)

Uses the variable we defined earlier apiPath to map all the routes we’ll create in ./routes. Therefore, all the routes you create will have the prefix /api/<route_name> . The full URI will look like this: localhost:3000/api/<route_name>.

The app.all matches requests made under all HTTP verbs (POST, GET, PUT, DELETE). We use this to ensure all requests made under all routes * are passed through the request handler we defined earlier.

app.all('*', (req: Request, res: Response) => {
return handle(req, res)
})

Then, we listen to the port we defined and log that the server has started.

app.listen(port, (err?: any) => {
if (err) throw err
console.log(`> Ready on localhost:${port} - env
${process.env.NODE_ENV}`)
})

Your entire file should look like the code shown below:

// ./server/index.ts  
import next from 'next'
import express, { Request, Response } from 'express'
import dotenv from 'dotenv'
import routes from './routes/'
dotenv.config() // import your environment variablesconst dev = process.env.NODE_ENV
const domain = process.env.SERVER_DOMAIN
const apiPath = process.env.API_PATH
const port = process.env.SERVER_PORT
const next = next({ dev })
const handle = next.getRequestHandler()
next.prepare().then(() => {
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true })) // Uses URL encoded query strings
// All your routes will be listed under `/api/*`
app.use(apiPath, routes)
app.all('*', (req: Request, res: Response) => {
return handle(req, res)
})
// listen on the port specified
app.listen(port, (err?: any) => {
if (err) throw err
console.log(`> Ready on localhost:${port} - env
${process.env.NODE_ENV}`)
})
})

Creating Routes

In /server/routes create index.ts and hello-world.routes.ts files.

In the hello-world.ts file, add the following:

// ./server/routes/hello-world.routes.tsimport express, { Request, Response } from 'express'
const router = express.Router()
router.get('/', (req: Request, res: Response, next) => {
return res.json({ result: 'Hello World!' })
})
export default router

In this file, instantiate an express router instance.

const router = express.Router()

The next bit of code tells us that GET requests to the root of this route will use handler specified.

router.get('/', (req: Request, res: Response, next) => {
return res.json({ result: 'Hello World!' })
})

res.json tells us that the response will return a JSON object. You can also use res.send and send any type of response back (integer, boolean, array, etc.).

Next, in /routes/index.ts file, include the file we created in the previous step, and use a router to specify this sub-routes name.

// ./server/routes/index.tsimport express from 'express'
import helloWorld from './hello-world.routes.ts'
const router = express.Router() router.use(`/hello-world`, helloWorld) export default router

Putting It All Together

Now you’ve come to the best part; testing it all out!

Spin up your application by running npm run dev . Thanks to the power of NextJS, your server and client will start simultaneously. And thanks to Nodemon, changes to server files will restart the server.

Lastly, if you open your browser or Postman and type, localhost:3000/api/hello-world/ you should see a successful response!

Hopefully, this quick tutorial was helpful to you! Let me know in the comments of any issues, tips, or tricks you encountered setting up your server.

Happy coding! ❤

Photo by Christopher Gower on Unsplash

Sign up to discover human stories that deepen your understanding of the world.

Landy
Landy

Written by Landy

Software Engineer | LinkedIn: simpslandyy | IG: miss.simpsonn. |

No responses yet

Write a response