# How to deploy your Node.js app (https://developer.godaddy.com/en/docs/api-users/hosting/workflows/deploy-nodejs-app)

---
title: How to deploy your Node.js app
description: >-
  Deploy your app end to end — create an app, attach a hosting plan, upload
  code, publish, and verify it's running.
keywords: >-
  POST /v1/hosting/apps, GET /app-operations, operation status, POST
  /apps/{appId}/imports, import upload, PUT /apps/{appId}/subscription, POST
  /apps/{appId}/deployments, deployment status, async polling pattern
agentNotes:
  scopes:
    - 'hosting.application:create'
    - 'hosting.application:read'
    - 'hosting.source:write'
    - 'hosting.deployment:execute'
    - 'hosting.subscription:write'
  rateLimit: 10–120 req/min per client IP depending on operation
  failureRecovery: >-
    Every write in this walkthrough is async. Poll the status endpoint for each
    step until it reaches a terminal state.
related:
  concepts:
    - title: Hosting core concepts
      href: /docs/api-users/hosting/concepts
    - title: Node.js app requirements
      href: /docs/api-users/hosting/app-requirements
  guides:
    - title: Manage apps
      href: /docs/api-users/hosting/manage-apps
    - title: Attach to a hosting plan
      href: /docs/api-users/hosting/attach-hosting-plan
    - title: Attach domain
      href: /docs/api-users/hosting/attach-domain
  apis:
    - title: Hosting API reference
      href: /docs/references/rest/hosting
---

## Overview

This walkthrough deploys an app from scratch. By the end, you'll have a running app on the `publish` variant. Every write operation is async. The API returns a operation or deployment id immediately, and you poll until the operation completes.

All code blocks use `$BASE_URL` for the API gateway host and `$GODADDY_PAT` for your Personal Access Token. Go to [Hosting core concepts](https://developer.godaddy.com/docs/api-users/hosting/concepts#scopes-reference) for the scopes you need.

## Prerequisites

The following prerequisites are required before you can deploy your Node.js app:

* a [Personal Access Token](https://developer.godaddy.com/personal-access-token) with the following scopes: `hosting.application:create`, `hosting.application:read`, `hosting.source:write`, `hosting.deployment:execute`, `hosting.subscription:write`
* a zipped Node.js application that meets the [Node.js app requirements](https://developer.godaddy.com/docs/api-users/hosting/app-requirements)
* a web hosting subscription to attach the app to

## Deploy your app

The following procedure creates an app, uploads source, attaches it to a hosting plan, publishes it to production, and confirms the app is running.

### Create an app

`POST /apps?appType=NODEJS` accepts a name and returns `202 Accepted` with an `AppOperation`, including a `links[rel=self]` URL. Poll that URL with `GET /app-operations/{operationId}` until the status reaches `COMPLETED` (success) or `FAILED`.

1. Create the app:

   ```bash tab="curl"
   curl -s -X POST "$BASE_URL/v1/hosting/apps?appType=NODEJS" \
     -H "Authorization: Bearer $GODADDY_PAT" \
     -H "Content-Type: application/json" \
     -d '{ "name": "my-first-app" }'
   ```

   ```js tab="Node"
   const res = await fetch(`${process.env.BASE_URL}/v1/hosting/apps?appType=NODEJS`, {
     method: 'POST',
     headers: {
       Authorization: `Bearer ${process.env.GODADDY_PAT}`,
       'Content-Type': 'application/json',
     },
     body: JSON.stringify({ name: 'my-first-app' }),
   });
   const { operationId, links } = await res.json();
   const pollUrl = links.find(l => l.rel === 'self').href;
   ```

   ```python tab="Python"
   import requests, os

   resp = requests.post(
       f"{os.environ['BASE_URL']}/v1/hosting/apps?appType=NODEJS",
       headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
       json={"name": "my-first-app"},
   )
   data = resp.json()
   operation_id = data["operationId"]
   poll_url = next(l["href"] for l in data["links"] if l["rel"] == "self")
   ```

   ```go tab="Go"
   body := strings.NewReader(`{"name":"my-first-app"}`)
   req, _ := http.NewRequest("POST", os.Getenv("BASE_URL")+"/v1/hosting/apps?appType=NODEJS", body)
   req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
   req.Header.Set("Content-Type", "application/json")
   resp, _ := http.DefaultClient.Do(req)
   defer resp.Body.Close()
   var result map[string]interface{}
   json.NewDecoder(resp.Body).Decode(&result)
   operationId := result["operationId"].(string)
   // Find poll URL from links array where rel == "self"
   ```

2. Poll until the operation reaches `COMPLETED` or `FAILED`:

   ```bash tab="curl"
   curl -s "$BASE_URL/v1/hosting/app-operations/$OPERATION_ID" \
     -H "Authorization: Bearer $GODADDY_PAT" | jq '{status: .status, appId: .app.id}'
   ```

   ```js tab="Node"
   let data;
   do {
     await new Promise(r => setTimeout(r, 3000));
     const r = await fetch(pollUrl, {
       headers: { Authorization: `Bearer ${process.env.GODADDY_PAT}` },
     });
     data = await r.json();
   } while (data.status !== 'COMPLETED' && data.status !== 'FAILED');
   const appId = data.app.id;
   ```

   ```python tab="Python"
   import time

   while True:
       r = requests.get(
           poll_url,
           headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
       )
       data = r.json()
       if data["status"] in ("COMPLETED", "FAILED"):
           break
       time.sleep(3)
   app_id = data["app"]["id"]
   ```

   ```go tab="Go"
   var appID string
   for {
       req, _ := http.NewRequest("GET", pollUrl, nil)
       req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
       resp, _ := http.DefaultClient.Do(req)
       var result map[string]interface{}
       json.NewDecoder(resp.Body).Decode(&result)
       resp.Body.Close()
       status := result["status"].(string)
       if status == "COMPLETED" || status == "FAILED" {
           app := result["app"].(map[string]interface{})
           appID = app["id"].(string)
           break
       }
       time.Sleep(3 * time.Second)
   }
   ```

### Upload source and preview your app

`POST /apps/{appId}/imports` accepts a zip as `multipart/form-data` and returns `202 Accepted` with a `SourceImport` operation. Poll it with `GET /apps/{appId}/imports/{importId}` until the status reaches `COMPLETED` or `FAILED`.

1. Upload the zip:

   ```bash tab="curl"
   curl -s -X POST "$BASE_URL/v1/hosting/apps/$APP_ID/imports" \
     -H "Authorization: Bearer $GODADDY_PAT" \
     -F "file=@./my-app.zip"
   ```

   ```js tab="Node"
   import { createReadStream } from 'fs';
   import FormData from 'form-data';

   const form = new FormData();
   form.append('file', createReadStream('./my-app.zip'));
   const res = await fetch(`${process.env.BASE_URL}/v1/hosting/apps/${appId}/imports`, {
     method: 'POST',
     headers: {
       Authorization: `Bearer ${process.env.GODADDY_PAT}`,
       ...form.getHeaders(),
     },
     body: form,
   });
   const { importId, links } = await res.json();
   const pollUrl = links.find(l => l.rel === 'self').href;
   ```

   ```python tab="Python"
   with open('./my-app.zip', 'rb') as f:
       resp = requests.post(
           f"{os.environ['BASE_URL']}/v1/hosting/apps/{app_id}/imports",
           headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
           files={"file": f},
       )
   data = resp.json()
   import_id = data["importId"]
   poll_url = next(l["href"] for l in data["links"] if l["rel"] == "self")
   ```

   ```go tab="Go"
   file, _ := os.Open("./my-app.zip")
   defer file.Close()
   buf := &bytes.Buffer{}
   w := multipart.NewWriter(buf)
   part, _ := w.CreateFormFile("file", "my-app.zip")
   io.Copy(part, file)
   w.Close()
   req, _ := http.NewRequest("POST",
       os.Getenv("BASE_URL")+"/v1/hosting/apps/"+appID+"/imports", buf)
   req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
   req.Header.Set("Content-Type", w.FormDataContentType())
   resp, _ := http.DefaultClient.Do(req)
   defer resp.Body.Close()
   var result map[string]interface{}
   json.NewDecoder(resp.Body).Decode(&result)
   importId := result["importId"].(string)
   // Find poll URL from links array where rel == "self"
   ```

2. Poll until the upload reaches a terminal state:

   ```bash tab="curl"
   curl -s "$BASE_URL/v1/hosting/apps/$APP_ID/imports/$IMPORT_ID" \
     -H "Authorization: Bearer $GODADDY_PAT" | jq .status
   ```

   ```js tab="Node"
   let importStatus;
   do {
     await new Promise(r => setTimeout(r, 2000));
     const r = await fetch(pollUrl, {
       headers: { Authorization: `Bearer ${process.env.GODADDY_PAT}` },
     });
     ({ status: importStatus } = await r.json());
   } while (importStatus !== 'COMPLETED' && importStatus !== 'FAILED');
   ```

   ```python tab="Python"
   while True:
       r = requests.get(
           poll_url,
           headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
       )
       import_status = r.json()["status"]
       if import_status in ("COMPLETED", "FAILED"):
           break
       time.sleep(2)
   ```

   ```go tab="Go"
   for {
       req, _ := http.NewRequest("GET", pollUrl, nil)
       req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
       resp, _ := http.DefaultClient.Do(req)
       var result map[string]interface{}
       json.NewDecoder(resp.Body).Decode(&result)
       resp.Body.Close()
       if s := result["status"].(string); s == "COMPLETED" || s == "FAILED" {
           break
       }
       time.Sleep(2 * time.Second)
   }
   ```

### Attach to hosting plan

This step is required before going live for Node.js apps. Future product types may have different provisioning requirements.

Before you can publish a Node.js app, you must attach it to a web hosting subscription. Use a valid subscription ID from your GoDaddy account (`GET /subscriptions?hostingProduct=WEB_HOSTING` lists the plans on it). `PUT /apps/{appId}/subscription` attaches the app to one, and the call is synchronous, so there's no operation to poll. Go to [Attach to a hosting plan](https://developer.godaddy.com/docs/api-users/hosting/attach-hosting-plan) for detailed guidance.

* Attach the app to a subscription:

  ```bash tab="curl"
  curl -s -X PUT "$BASE_URL/v1/hosting/apps/$APP_ID/subscription" \
    -H "Authorization: Bearer $GODADDY_PAT" \
    -H "Content-Type: application/json" \
    -d '{ "subscriptionId": "'$SUBSCRIPTION_ID'" }'
  ```

  ```js tab="Node"
  const res = await fetch(`${process.env.BASE_URL}/v1/hosting/apps/${appId}/subscription`, {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${process.env.GODADDY_PAT}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ subscriptionId }),
  });
  if (!res.ok) {
    console.error(`Error: ${res.status}`);
    return;
  }
  const result = await res.json();
  ```

  ```python tab="Python"
  import requests, os

  resp = requests.put(
      f"{os.environ['BASE_URL']}/v1/hosting/apps/{app_id}/subscription",
      headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
      json={"subscriptionId": subscription_id},
  )
  if resp.status_code != 200:
      print(f"Error: {resp.status_code}")
      exit(1)
  result = resp.json()
  ```

  ```go tab="Go"
  body := strings.NewReader(`{"subscriptionId":"` + subscriptionID + `"}`)
  req, _ := http.NewRequest("PUT",
      os.Getenv("BASE_URL")+"/v1/hosting/apps/"+appID+"/subscription",
      body)
  req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
  req.Header.Set("Content-Type", "application/json")
  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()
  if resp.StatusCode != 200 {
      fmt.Printf("Error: %d\n", resp.StatusCode)
      return
  }
  var result map[string]interface{}
  json.NewDecoder(resp.Body).Decode(&result)
  ```

### Publish your app

`POST /apps/{appId}/deployments` builds the latest source, deploys it to the publish environment, and returns `202 Accepted` with a `Deployment`. Poll it with `GET /apps/{appId}/deployments/{deploymentId}` until the status reaches `COMPLETED` or `FAILED`.

1. Publish the app:

   ```bash tab="curl"
   curl -s -X POST "$BASE_URL/v1/hosting/apps/$APP_ID/deployments" \
     -H "Authorization: Bearer $GODADDY_PAT" \
     -H "Content-Type: application/json" \
     -d '{}'
   ```

   ```js tab="Node"
   const res = await fetch(`${process.env.BASE_URL}/v1/hosting/apps/${appId}/deployments`, {
     method: 'POST',
     headers: {
       Authorization: `Bearer ${process.env.GODADDY_PAT}`,
       'Content-Type': 'application/json',
     },
     body: JSON.stringify({}),
   });
   const { deploymentId, links } = await res.json();
   const pollUrl = links.find(l => l.rel === 'self').href;
   ```

   ```python tab="Python"
   resp = requests.post(
       f"{os.environ['BASE_URL']}/v1/hosting/apps/{app_id}/deployments",
       headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
       json={},
   )
   data = resp.json()
   deployment_id = data["deploymentId"]
   poll_url = next(l["href"] for l in data["links"] if l["rel"] == "self")
   ```

   ```go tab="Go"
   req, _ := http.NewRequest("POST",
       os.Getenv("BASE_URL")+"/v1/hosting/apps/"+appID+"/deployments",
       strings.NewReader(`{}`))
   req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
   req.Header.Set("Content-Type", "application/json")
   resp, _ := http.DefaultClient.Do(req)
   var result map[string]interface{}
   json.NewDecoder(resp.Body).Decode(&result)
   deploymentId := result["deploymentId"].(string)
   // Find poll URL from links array where rel == "self"
   ```

2. Poll until deployment reaches a terminal state:

   ```bash tab="curl"
   curl -s "$BASE_URL/v1/hosting/apps/$APP_ID/deployments/$DEPLOYMENT_ID" \
     -H "Authorization: Bearer $GODADDY_PAT" | jq .status
   ```

   ```js tab="Node"
   let deploymentStatus;
   do {
     await new Promise(r => setTimeout(r, 5000));
     const r = await fetch(pollUrl, {
       headers: { Authorization: `Bearer ${process.env.GODADDY_PAT}` },
     });
     ({ status: deploymentStatus } = await r.json());
   } while (deploymentStatus !== 'COMPLETED' && deploymentStatus !== 'FAILED');
   ```

   ```python tab="Python"
   while True:
       r = requests.get(
           poll_url,
           headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
       )
       deployment_status = r.json()["status"]
       if deployment_status in ("COMPLETED", "FAILED"):
           break
       time.sleep(5)
   ```

   ```go tab="Go"
   for {
       req, _ := http.NewRequest("GET", pollUrl, nil)
       req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
       resp, _ := http.DefaultClient.Do(req)
       var result map[string]interface{}
       json.NewDecoder(resp.Body).Decode(&result)
       resp.Body.Close()
       if s := result["status"].(string); s == "COMPLETED" || s == "FAILED" {
           break
       }
       time.Sleep(5 * time.Second)
   }
   ```

### Verify

`GET /apps/{appId}/status` returns the application lifecycle in top-level `status` and per-environment detail in `variants`. Inspect the `PUBLISH` entry in `variants` to confirm that environment is up.

* Read the app status:

  ```bash tab="curl"
  curl -s "$BASE_URL/v1/hosting/apps/$APP_ID/status" \
    -H "Authorization: Bearer $GODADDY_PAT" | jq .
  ```

  ```js tab="Node"
  const r = await fetch(`${process.env.BASE_URL}/v1/hosting/apps/${appId}/status`, {
    headers: { Authorization: `Bearer ${process.env.GODADDY_PAT}` },
  });
  console.log(await r.json());
  ```

  ```python tab="Python"
  resp = requests.get(
      f"{os.environ['BASE_URL']}/v1/hosting/apps/{app_id}/status",
      headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
  )
  print(resp.json())
  ```

  ```go tab="Go"
  req, _ := http.NewRequest("GET",
      os.Getenv("BASE_URL")+"/v1/hosting/apps/"+appID+"/status", nil)
  req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
  resp, _ := http.DefaultClient.Do(req)
  io.Copy(os.Stdout, resp.Body)
  resp.Body.Close()
  ```

### Get app URLs

`GET /apps/{appId}` returns the full app record, including a `urls` object with the URL for each environment. Your code is not live at `urls.publish` until you publish.

* Read the app record to get its URLs:

  ```bash tab="curl"
  curl -s "$BASE_URL/v1/hosting/apps/$APP_ID" \
    -H "Authorization: Bearer $GODADDY_PAT" | jq .urls
  ```

  ```js tab="Node"
  const res = await fetch(`${process.env.BASE_URL}/v1/hosting/apps/${appId}`, {
    headers: { Authorization: `Bearer ${process.env.GODADDY_PAT}` },
  });
  const { urls } = await res.json();
  console.log(urls.preview);  // preview environment URL
  console.log(urls.publish);  // publish environment URL
  ```

  ```python tab="Python"
  resp = requests.get(
      f"{os.environ['BASE_URL']}/v1/hosting/apps/{app_id}",
      headers={"Authorization": f"Bearer {os.environ['GODADDY_PAT']}"},
  )
  urls = resp.json()["urls"]
  print(urls["preview"])  # preview environment URL
  print(urls["publish"])  # publish environment URL
  ```

  ```go tab="Go"
  req, _ := http.NewRequest("GET",
      os.Getenv("BASE_URL")+"/v1/hosting/apps/"+appID, nil)
  req.Header.Set("Authorization", "Bearer "+os.Getenv("GODADDY_PAT"))
  resp, _ := http.DefaultClient.Do(req)
  defer resp.Body.Close()
  var result map[string]interface{}
  json.NewDecoder(resp.Body).Decode(&result)
  urls := result["urls"].(map[string]interface{})
  fmt.Println(urls["preview"])  // preview environment URL
  fmt.Println(urls["publish"])  // publish environment URL
  ```

## Deploy updates

To deploy new code to an existing app, repeat the upload and publish steps. You don't need to create a new app or re-attach it to a hosting plan.

1. Upload the updated zip using `POST /apps/{appId}/imports`.

   Go to [Upload source and preview your app](#upload-source-and-preview-your-app) for the full procedure.

2. Publish the update using `POST /apps/{appId}/deployments`.

   Go to [Publish your app](#publish-your-app) for the full procedure.

## Common errors

The following table lists common errors and recommended actions:

| Status                       | Cause                                    | Action                                                                                                   |
| ---------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `401`                        | Expired or revoked PAT, or missing scope | Go to [Hosting core concepts](https://developer.godaddy.com/docs/api-users/hosting/concepts#scopes-reference) for the scopes you need |
| `429`                        | Rate limit exceeded                      | Back off and retry. Go to [Rate limits](https://developer.godaddy.com/docs/api-users/rate-limits) for handling guidance               |
| Operation stuck in `PENDING` | Upstream provisioning delay              | Wait up to two minutes before treating it as failed                                                      |
| Import job `FAILED`          | Malformed zip or build failure           | Check the application structure and reupload                                                             |
