Tutorial - Configuring TypeScript, Express, Jest, Payload, and VSCode Debugging from Scratch
Tags: tutorial, typescript, webdev, beginners
Published: 2022-06-02 12:48:37

#### Setting up a TypeScript project from scratch can be intimidating. Bring in Jest, VSCode debugging, and a headless CMS - and there are many moving parts. But it doesn't have to be difficult!

We've been working hard at making Payload + TypeScript a match made in heaven, and over the last few months we've released a suite of features, including [automatic type generation](https://payloadcms.com/docs/typescript/generating-types), which makes Payload by far the best TypeScript headless CMS available. To celebrate, we're going to show you how to scaffold a TypeScript and Express project from scratch, including testing it with Jest and debugging with VSCode.

By understanding just a few new concepts, you can master your dev environment's setup to maximize productivity and gain a deep understanding how it all works together. Let's get started.

#### Software Requirements

Before going further, make sure you have the following software:

- Yarn or NPM
- NodeJS
- A Mongo Database

#### Step 1 - initialize a new project

Create a new folder, `cd` into it, and initialize:

```
mkdir ts-payload && cd ts-payload && yarn init
```

#### Step 2 - install dependencies

We'll need a few baseline dependencies:

- `dotenv` - to set up our environment easily
- `express` - Payload is built on top of Express
- `ts-node` - to execute our TypeScript project in development mode
- `typescript` - base TS dependency
- `payload` - no description necessary
- `nodemon` - to make sure our project restarts automatically when files change

Install these dependencies by running:

```
yarn add dotenv express payload
```

and the rest as devDependencies using:

```
yarn add --dev nodemon ts-node typescript
```

#### Step 3 - create a tsconfig:

In the root of your project folder, create a new file called `tsconfig.json` and add the following content to it. This file tells the TS compiler how to behave, including where to write its output files.

Example `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "outDir": "./dist",
    "skipLibCheck": true,
    "strict": false,
    "esModuleInterop": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "sourceMap": true
  },
  "include": [
    "src"
  ],
  "ts-node": {
    "transpileOnly": true
  }
}
```

In the above example config, we're planning to keep all of our TypeScript files in `/src`, and then build to `/dist`.

#### Step 4 - set up your .env file

We'll be using `dotenv` to manage our environment and get ourselves set up for deployment to various different environments like staging and production later down the road. The `dotenv` package will  read all values in a `.env` file within our project root and bind their values to `process.env` so that you can access them in your code.

**Important:**

> If you're using GitHub, make sure you ignore your environment file from your  repository. Often, environment files store sensitive information and should not be included in source code.

Let's create a `.env` file in the root folder of your project and add the following:

```
PORT=3000
MONGO_URL=mongodb://localhost/your-project-name
PAYLOAD_SECRET_KEY=alwifhjoq284jgo5w34jgo43f3
PAYLOAD_CONFIG_PATH=src/payload.config.ts
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
```

Make sure that the `MONGO_URL` line in your `.env` matches an available MongoDB instance. If you have Mongo running locally on your computer, the line above should work right out of the box, but if you want to use a hosted MongoDB like Mongo Atlas, make sure you copy and paste the connection string from your database and update your `.env` accordingly.

For more information on what these values do, take a look at [Payload's Getting Started docs](https://payloadcms.com/docs/getting-started/installation#server).

#### Step 5 - create your server

Setting up an Express server might be pretty familiar. Create a `src/server.ts` file in your project and add the following to the file:

```ts
import express from 'express';
import payload from 'payload';
import path from 'path';

// Use `dotenv` to import your `.env` file automatically
require('dotenv').config({
  path: path.resolve(__dirname, '../.env'),
});

const app = express();

payload.init({
  secret: process.env.PAYLOAD_SECRET_KEY,
  mongoURL: process.env.MONGO_URL,
  express: app,
})

app.listen(process.env.PORT, async () => {
	console.log(`Express is now listening for incoming connections on port ${process.env.PORT}.`)
});
```

The file above first imports our server dependencies. Then, we use `dotenv` to load our `.env` file at our project root. Next, we initialize Payload by providing it with our secret key, Mongo connection string, and Express app. Finally, we tell our Express app to listen on the port defined in our `.env` file.

#### Step 6 - create a nodemon file

We'll use Nodemon to automatically restart our server when any `.ts` files change within our `./src` directory. Nodemon will execute `ts-node` for us, which will use our server as its entry point. Create a `nodemon.json` file within the root of your project and add the following content.

Example `nodemon.json`:

```json
{
  "watch": [
    "./src/**/*.ts"
  ],
  "exec": "ts-node ./src/server.ts"
}
```

#### Step 7 - add your Payload config

The Payload config is central to everything Payload does. Add it to your `src` folder and enter the following baseline code:

`./src/payload.config.ts`:

```ts
import dotenv from 'dotenv';
import path from 'path';
import { buildConfig } from 'payload/config';

dotenv.config({
  path: path.resolve(__dirname, '../.env'),
});

export default buildConfig({
  serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
	typescript: {
		outputFile: path.resolve(__dirname, './generated-types.ts'),
	},
  collections: [
    {
      slug: 'posts',
      admin: {
        useAsTitle: 'title',
      },
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'author',
          type: 'relationship',
          relationTo: 'users',
        },
      ]
    }
  ]
});
```

This config is very basic - but check out [the Config docs](https://payloadcms.com/docs/configuration/overview) for more on what the Payload config can do. Out of the box, this config will give you a default `Users` collection, a simple `Posts` collection with a few fields, and will open up the admin panel to you at `http://localhost:3000/admin`.

The config also specifies where Payload should output its auto-generated TypeScript types which is super cool (we'll come back to this).

#### Step 8 - add some NPM scripts

The last step before we can fire up our project is to add some development, build, and production NPM scripts.

Open your `package.json` and update the `scripts` property to the following:

```
{
  "scripts": {
    "generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
    "dev": "nodemon",
    "build:payload": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
    "build:server": "tsc",
    "build": "yarn build:server && yarn build:payload",
    "serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js"
  },
}
```

To support Windows environments consider adding the `cross-env` package as a devDependency and use it in scripts before setting variables.

The first script uses Payload's `generate:types` command in order to automatically generate TypeScript types for each of your collections and globals automatically. You can run this command whenever you need to regenerate your types, and then you can use these types in your Payload code directly.

**Tip:**

> Payload's ability to automatically generate types from your configs is super powerful and will benefit you immensely if you are writing custom hooks or custom components.

The next script is to execute `nodemon`, which will read the `nodemon.json` config that we've written and execute our `/src/server.ts` script, which fires up Payload in development mode.

The following three scripts are how we will prepare Payload's admin panel for production as well as how to compile the server's TypeScript code into regular JS to use in production.

Finally, we have a `serve` script which is used to serve our app in production mode once it's built.

### Firing it up

We're ready to go! Run `yarn dev` in the root of your folder to start up Payload. Then, visit `http://localhost:3000/admin` to create your first user and sign into the admin panel.

#### Generating Payload types

Now that we've got a server generated, let's try and generate some types. Run the following command to automatically generate a file that contains an interface for the default `Users` collection and our simple `Posts` collection:

```
yarn generate:types
```

Then check out the file that was created at `/src/generated-types.ts`. You can import these types in your own code to do some pretty awesome stuff.

## Testing with Jest

Now it's time to get some tests written. There are a ton of different approaches to testing, but we're going to go straight for end-to-end tests. We'll use Jest to write our tests, and set up a fully functional Express server before we start our tests so that we can test against something that's as close to production as possible.

We'll also use `mongodb-memory-server` to connect to for all tests, so that we don't have to clutter up our development database with testing documents. This is great, because our tests will be totally controlled and isolated, but coverage will be incredibly thorough due to how we'll be testing the full API from top to bottom.

Payload will automatically attempt to use `mongodb-memory-server` if two conditions are met:

1. It is locally installed in your project
2. `NODE_ENV` is equal to `test`

#### Adding and configuring test dependencies

OK. Let's install all the testing dependencies we'll need:

```
yarn add --dev jest mongodb-memory-server babel-jest @babel/core @babel/preset-env @babel/preset-typescript isomorphic-fetch @types/jest
```

Now let's add two new config files. First, `babel.config.js`:

```js
module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
    '@babel/preset-react',
  ],
};
```

We use Babel so we can write tests in TypeScript, and test full React components.

Next, `jest.config.js`:

```js
module.exports = {
  verbose: true,
  testEnvironment: 'node',
  globalSetup: '<rootDir>/src/tests/globalSetup.ts',
  roots: ['<rootDir>/src/'],
};
```

#### Global Test Setup

A sharp eye might find that we're using a `globalSetup` file in `jest.config.js` to scaffold our project before any of the real magic starts. Let's add that file:

`src/tests/globalSetup.ts`:

```ts
import '../server';
import { resolve } from 'path';
import testCredentials from './credentials';

require('dotenv').config({
  path: resolve(__dirname, '../.env'),
});

const { PAYLOAD_PUBLIC_SERVER_URL } = process.env;

const globalSetup = async (): Promise<void> => {
  const response = await fetch(`${PAYLOAD_PUBLIC_SERVER_URL}/api/users/first-register`, {
    body: JSON.stringify({
      email: testCredentials.email,
      password: testCredentials.password,
    }),
    headers: {
      'Content-Type': 'application/json',
    },
    method: 'post',
  });

  const data = await response.json();

  if (!data.user || !data.user.token) {
    throw new Error('Failed to register first user');
  }
};

export default globalSetup;
```

In this file, we're performing the following actions before our tests are executed:

1. Importing the server, which will boot up Express and Payload using `mongodb-memory-server`
1. Loading our `.env` file
1. Registering a first user so that we can authenticate in our tests

You'll notice we are importing `testCredentials` from next to our `globalSetup` file. Because our Payload API will require authentication for many of our tests, and we're creating that user in our `globalSetup` file, we will want to reuse our user credentials in other tests to ensure we can authenticate as the newly created user. Let's create a reusable file to store our user's credentials:

`src/tests/credentials.ts`:

```js
export default {
  email: 'test@test.com',
  password: 'test',
}
```

#### Our first test

Now that we've got our global setup in place, we can write our first test.

Add a file called `src/tests/login.spec.ts`:

```ts
import { User } from '../generated-types';
import testCredentials from './credentials';

require('isomorphic-fetch');

describe('Users', () => {
  it('should allow a user to log in', async () => {
    const result: {
      token: string
      user: User
    }  = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/users/login`, {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: testCredentials.email,
        password: testCredentials.password,
      }),
    }).then((res) => res.json());

    expect(result.token).toBeDefined();
  });
});

```

The test above is written in TypeScript and imports our auto-generated `User` TypeScript interface to properly type the `fetch` response that is returned from Payload's `login` REST API.

It will expect that a token is returned from the response.

#### Running tests

The last step is to add a script to execute our tests. Let's add a new line to our `package.json` `scripts` property:

```
    "test": "jest --forceExit --detectOpenHandles"
```

Now, we can run `yarn test` to see a successful test!

## Debugging with VSCode

Debugging can be an absolutely invaluable tool to developers working on anything more complex than a simple app. It can be difficult to understand how to set up, but once you have it configured properly, a proper debugging workflow can be significantly more powerful than just relying on `console.log` all the time.

You can debug your application itself, and you can even debug your tests to troubleshoot any tests that might fail in the future. Let's see how to set up VSCode to debug our new Typescript app and its tests.

First, create a new folder within your project root called `.vscode`. Then, add a `launch.json` within that folder, containing the following configuration:

`./.vscode/launch.json`:

```json
{
  "version": "0.2.0",
  "configurations": [
        {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "env": {
        "PAYLOAD_CONFIG_PATH": "src/payload.config.ts",
      },
      "program": "${workspaceFolder}/src/server.ts",
    },
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "${workspaceRoot}/node_modules/.bin/jest",
        "--runInBand"
      ],
      "env": {
        "PAYLOAD_CONFIG_PATH": "src/payload.config.ts"
      },
    }
  ]
}

```

The file above includes two configurations. The first is to be able to debug your Express + Payload app itself, including any files you've imported within your Payload config. The second debug config is to be able to set breakpoints and debug directly within your Jest testing suite.

To debug, you can set breakpoints right in your code by clicking to the left of the line numbers. A breakpoint will be set and show as a red circle. When VSCode executes your scripts, it will pause at the breakpoint(s) you set and allow you to inspect the value of all variables, where your function(s) have been called from, and much more.

To start the debugger, click the "Run and Debug" sidebar icon in VSCode, choose the debugger you want to start, and click the "Play" button. If you've placed breakpoints, VSCode will automatically pause when it reaches your breakpoint.

Here is an example of a breakpoint being hit within our `src/server.ts` file:

![Debugging Express and Payload using TypeScript in VSCode](https://payloadcms.com/images/blog/ts-tutorial/debug-express.jpg)

Here is a screenshot of a breakpoint being hit within our `login.spec.ts` test:

![Debugging a TypeScript Jest app in VSCode](https://payloadcms.com/images/blog/ts-tutorial/debug-jest.jpg)

**Debugging can be an invaluable tool to you as a developer.** Setting it up early in your project will pay dividends over time as your project gets more complex, and it will help you understand how your project works to an extremely fine degree.

## Conclusion

With all of the above pieces in place, you have a modern and well-equipped dev environment that you can use to build out Payload into anything you can think of - be it an API to power a [web app](https://payloadcms.com/use-cases/web-apps), [native app](https://payloadcms.com/use-cases/native-apps), or just a headless CMS to [power a website](https://payloadcms.com/use-cases/websites).

[You can find the code for this guide here](https://github.com/payloadcms/typescript-jest-vscode). Let us know what you think!

### Star us on GitHub

If you haven't already, stop by GitHub and [give us a star](https://github.com/payloadcms/payload) by clicking on the "Star" button in the top-right corner of our repo. With our [inclusion into YC](https://payloadcms.com/blog/announcing-yc) and our [move to open-source](https://payloadcms.com/blog/open-source), we're looking to dramatically expand our community and we can't do it without you.

### Join our community on Discord

We've recently started a Discord community for the Payload community to interact in realtime. Often, Discord will be the first to hear about announcements like this move to open-source, and it can prove to be a great resource if you need help building with Payload. [Click here](https://discord.com/invite/r6sCXqVk3v) to join!