# Building a simple code Runner with typescript , docker , react and express

In this project, we are going to build a simple code runner that will be able to run nodejs , golang and python code

![](https://github-production-user-asset-6210df.s3.amazonaws.com/30620860/238506067-70373443-3988-4919-b859-4f43d92e2c69.png align="center")

we are going to use Express for backend and React for frontend

### Preparing environment to run code

we are going to use a docker container for both our code running environment and for our backend API , this is what our backend Dockerfile would look like

```yaml
FROM alpine:latest 

WORKDIR /app 
RUN apk add python3
RUN apk add --no-cache git make musl-dev go curl bash
RUN apk add --no-cache nodejs npm 

COPY ./backend/package*.json ./

# USER node

RUN npm install

COPY --chown=node:node ./backend .

EXPOSE 3000

CMD [ "npm","run", "dev" ]
```

we use Alpine as our base image and install `python3` , `golang` and `nodejs` latest version with `RUN` command. And copy the package.json of our backend to the image and install dependencies as we are also going to put our backend inside this container

### Backend

we are going to use express for backend API . There will be a route `/run-code` it would expect the language and code text in the request body, Joi will be used to validate the request body

```typescript
const RunCodeRequestValidator = Joi.object()
    .keys({
        lang : Joi.string()
              .valid('python','node','golang','typescript')
              .required() ,
        code : Joi.string().allow('') 
     })
```

in the router file

```typescript
   this.router.post(`${this.path}/run-code`,
            JoiValidator(RunCodeRequestValidator),
            this.controller.runCode
   )
```

after receiving the request we are going to create a temporary file to store the code and after running the file and getting the output we are going to remove the file So for file-related tasks we are going to create a file manager service

```typescript
import fs from 'fs/promises'
import { v4 as uuid } from 'uuid';

class FileManagerService {

    constructor() {

    }

    public async create(lang, code): Promise<{ success: boolean; path: string }> {
        try {
            const extensionMap = {
                'python': 'py',
                'node': 'js',
                'golang': 'go'
            }
            const path = `./files/${uuid()}.${extensionMap[lang]}`
            await fs.writeFile(path, code)
            return { success: true, path }
        } catch (error) {
            return { success: false, path: null }
        }
    }

    public async remove(path) {

        try {
            await fs.unlink(path)
            return { success: true }
        } catch (error) {
            console.log(error)
            return { success: false }
        }
        
    }
}
```

`FileManagerService` has two methods `create` and `remove`. On create method, we generate the file names with `uuid` and give them extensions based on the language type and store the files in `/files` directory of the project. The method returns a promise which resolves to an object which has `success` and `path` fields, if file creation is successful `success` is true else false. And on the remove method, we simply remove the file with `fs.unlink()` We will have a single class for every language which will all implement `RunnerInterface` with a `run` method inside. Every language class will have to implement its logic to run code related to it.

```typescript
interface RunnerInterface {
    run(path) : Promise<string>
}
```

In this way In the future when we decide to add more languages to our app we won't need to write code to a single file, every language would have a class associated with it , for Python there will be a `PythonRunnerService`

```typescript
import { exec } from 'child_process'
import RunnerInterface from '../../interfaces/runner.interface'

class PythonRunnerService implements RunnerInterface {

    public async run(path): Promise<string> {
        try {
            const res1 = await (new Promise((resolve, reject) => {
                exec(`python3 ${path}`, (error, stdout, stderr) => {
                    if (error) {
                        reject(error)
                    }
                    if (stderr) reject(stderr)
                    resolve(stdout)
                })
            }))
            return res1 as string
        } catch (error) {
            console.log(error)
            return error.toString()
        }
    }
}
```

hear we use the `exec` method from nodejs built-in module `child_process` to run the code and collect the output, the `run` method takes in `path` as a parameter which we get from `FileManagerService.create` method. To run the Python code we need to pass `python3 python/file/path` to exec and collect the output, we resolve the output if no error and reject with an error object if there is any error

Similarly for nodejs

```typescript
class NodeRunnerService implements RunnerInterface{

    public async run(path): Promise<string> {
        try {
            const res1 = await (new Promise((resolve, reject) => {
                exec(`node ${path}`, (error, stdout, stderr) => {
                    if (error) {
                        reject(error)
                    }
                    if (stderr) reject(stderr)
                    resolve(stdout)
                })
            }))
            // console.log({ res1 })
            return res1 as string
        } catch (error) {
            console.log(error)
            return error.toString()
        }
    }
}
```

and for `golang`

```typescript

class GoLangRunnerService implements RunnerInterface {
    async run(path: any): Promise<string> {
        try {
            const res1 = await (new Promise((resolve, reject) => {
                exec(`go run ${path}`, (error, stdout, stderr) => {
                    if (error) {
                        reject(error)
                    }
                    if (stderr) reject(stderr)
                    resolve(stdout)
                })
            }))
            return res1 as string
        } catch (error) {
            console.log(error)
            return error.toString()
        }
    }

}
```

and we map these language-related services from a Manager which is used in the controller

```typescript
class Manager {


    public static async getOutput(lang, code): Promise<string> {

        const fileManagerService = new FileManagerService()
        const { success, path } = await fileManagerService.create(lang, code);
        if(!success) return 'error'
        const mp = {
            'python': PythonRunnerService,
            'node' : NodeRunnerService ,
            'golang' : GoLangRunnerService 
        }
        const runner: RunnerInterface = new mp[lang]
        const out = await runner.run(path)
        await fileManagerService.remove(path)
        return out
    }
}
```

we now just need to get the language with a dropdown and code from a textarea and pass them down to the API when calling and show the response in another textarea and we have ourselves a code runner

### Docker compose

To bring the whole thing up with a single command we can use docker-compose we would also have a docker file for our frontend project

```yaml
FROM node:latest

RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

COPY package*.json ./

USER node

# RUN yarn install

COPY --chown=node:node . .

EXPOSE 3001

CMD [ "yarn", "dev" ]
```

so the project structure would look like this

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684203308112/b5b6e598-4cf6-406d-ac87-cdd2336ae380.png align="center")

now to combine the backend and frontend we write a docker-compose file

```yaml
services:
  server:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./backend:/app
  client:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports: 
      - 3001:3001
    volumes:
      - ./frontend:/home/node/app
      - ./frontend/node_modules:/home/node/app/node_modules
```

hear we have two services server and client. Bind mount was used here to mount code from the local drive to the container which is of great help for development as we could run both the backend and frontend with a single command and see code changes made to our local drive reflected inside the container. If we run docker-compose up we can access the client / frontend at [localhost:3001](http://localhost:3001) in our browse.

here is the GitHub repository link to the project

[https://github.com/aistiak/code-runner](https://github.com/aistiak/code-runner)
