I stopped treating Visual Studio Code as "just an editor" the day I embraced
launch.json
and tasks.json
. Since then,
I've wired most projects so that F5 builds, runs, opens my browser, spins up the frontend,
and even brings supporting services to life. In this post I'm distilling the patterns that stuck
with me after many full‑stack apps (Node + .NET 8), using a demo name DataVista
instead of a real project.
Mental model: launch vs tasks
- launch.json — how I debug/run (which program, args, CWD, env, auto‑open URL, etc.).
- tasks.json — reusable commands (build, watch, lint, tests, EF migrations, Docker scripts).
Glue: preLaunchTask
ties the two so F5 orchestrates my world.
launch.json — battle‑tested basics
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch DataVista API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-api",
"program": "${workspaceFolder}/src/backend/DataVista.API/bin/Debug/net8.0/DataVista.API.dll",
"cwd": "${workspaceFolder}/src/backend/DataVista.API",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": { "ASPNETCORE_ENVIRONMENT": "Development" },
"envFile": "${workspaceFolder}/src/backend/DataVista.API/.env",
"sourceFileMap": { "/Views": "${workspaceFolder}/Views" },
"justMyCode": true
},
{
"name": "Launch Frontend (Vite)",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"program": "${workspaceFolder}/src/frontend/node_modules/vite/bin/vite.js",
"args": ["dev"],
"cwd": "${workspaceFolder}/src/frontend",
"env": { "NODE_ENV": "development" },
"envFile": "${workspaceFolder}/src/frontend/.env",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Attach to DataVista API",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
launch.json — useful extras
1) Chrome debugging
{
"name": "Chrome against http://localhost:5173",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src/frontend"
}
I add a Chrome entry when I want JS breakpointing directly in the browser via VS Code.
tasks.json — battle‑tested basics
{
"version": "2.0.0",
"tasks": [
{
"label": "build-api",
"type": "process",
"command": "dotnet",
"args": ["build", "${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj",
"/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary"],
"problemMatcher": "$msCompile",
"group": { "kind": "build", "isDefault": true },
"runOptions": { "reevaluateOnRerun": true }
},
{
"label": "dev-frontend",
"type": "shell",
"command": "npm run dev",
"options": { "cwd": "${workspaceFolder}/src/frontend" },
"isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated", "focus": false }
}
]
}
Notes: I prefer
panel: "dedicated"
so restarts don't spam new terminals. Background tasks keep the debug session responsive.
tasks.json — useful extras
1) Depends on / sequence orchestration
{
"label": "bootstrap",
"dependsOrder": "sequence",
"dependsOn": ["restore-backend", "start-services", "dev-frontend"]
}
2) Dockerized services with env overrides
{
"label": "start-services",
"type": "shell",
"command": "docker compose -f docker-compose.yml up -d postgres redis",
"options": { "cwd": "${workspaceFolder}", "env": { "COMPOSE_PROFILES": "dev" } },
"problemMatcher": []
}
{
"label": "stop-services",
"type": "shell",
"command": "docker compose down",
"options": { "cwd": "${workspaceFolder}" },
"problemMatcher": []
}
3) EF Core with inputs (promptString & pickString)
{
"label": "ef-migrations-add",
"type": "process",
"command": "dotnet",
"args": [
"ef","migrations","add","${input:migrationName}",
"--project","${workspaceFolder}/src/backend/DataVista.Infrastructure/DataVista.Infrastructure.csproj",
"--startup-project","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj"
],
"problemMatcher": "$msCompile"
},
{
"label": "ef-migrations-script",
"type": "process",
"command": "dotnet",
"args": [
"ef","migrations","script",
"${input:fromMigration}","${input:toMigration}","-o","${workspaceFolder}/artifacts/migration.sql"
],
"problemMatcher": "$msCompile"
}
"inputs": [
{ "id": "migrationName", "type": "promptString", "description": "Migration name", "default": "AddUsers" },
{ "id": "fromMigration", "type": "pickString", "description": "From", "options": ["0","Initial","Previous"] },
{ "id": "toMigration", "type": "promptString", "description": "To (blank = latest)", "default": "" }
]
4) Type checking, linting, and tests
{
"label": "typecheck-frontend",
"type": "shell",
"command": "npx tsc --noEmit",
"options": { "cwd": "${workspaceFolder}/src/frontend" },
"problemMatcher": ["$tsc"],
"group": "build"
},
{
"label": "lint-frontend",
"type": "shell",
"command": "npm run lint",
"options": { "cwd": "${workspaceFolder}/src/frontend" }
},
{
"label": "test-backend",
"type": "process",
"command": "dotnet",
"args": ["test","${workspaceFolder}/src/backend/DataVista.Backend.sln","--nologo"],
"group": "test"
}
5) Presentation and terminal control
{
"label": "run-api",
"type": "process",
"command": "dotnet",
"args": ["run","--project","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj"],
"isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated", "clear": true }
}
6) Variable tricks I actually use
{
"label": "bundle-one-file",
"type": "shell",
"command": "esbuild ${file} --bundle --outfile=${workspaceFolder}/dist/${fileBasenameNoExtension}.js"
}
Other helpful variables:
${workspaceFolderBasename}
, ${relativeFileDirname}
, ${env:NAME}
.Compound configs — one key to start everything
{
"version": "0.2.0",
"configurations": [
{ "name": "Launch DataVista API", "...": "see above" },
{ "name": "Launch Frontend (Vite)", "...": "see above" },
{ "name": "Chrome against http://localhost:5173", "...": "see above" }
],
"compounds": [
{
"name": "Backend + Frontend",
"configurations": ["Launch DataVista API", "Launch Frontend (Vite)"],
"stopAll": true
},
{
"name": "Full stack + Browser",
"configurations": [
"Launch DataVista API",
"Launch Frontend (Vite)",
"Chrome against http://localhost:5173"
],
"stopAll": true
}
]
}
My daily full‑stack flow
- Bootstrap once: run the bootstrap task (restore → start services → dev frontend).
- Pick compound: choose Full stack + Browser.
- Press F5: API builds, Kestrel URL opens, Vite starts, Chrome attaches.
- Hot reload and iterate. EF migrations via tasks when needed.
- Shut down with stop-services if Docker containers aren't needed.
Complete, ready‑to‑paste snippets
.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch DataVista API",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-api",
"program": "${workspaceFolder}/src/backend/DataVista.API/bin/Debug/net8.0/DataVista.API.dll",
"cwd": "${workspaceFolder}/src/backend/DataVista.API",
"stopAtEntry": false,
"serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" },
"env": { "ASPNETCORE_ENVIRONMENT": "Development" },
"envFile": "${workspaceFolder}/src/backend/DataVista.API/.env",
"sourceFileMap": { "/Views": "${workspaceFolder}/Views" },
"justMyCode": true
},
{
"name": "Launch Frontend (Vite)",
"type": "node",
"request": "launch",
"runtimeExecutable": "node",
"program": "${workspaceFolder}/src/frontend/node_modules/vite/bin/vite.js",
"args": ["dev"],
"cwd": "${workspaceFolder}/src/frontend",
"env": { "NODE_ENV": "development" },
"envFile": "${workspaceFolder}/src/frontend/.env",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": ["/**"]
},
{
"name": "Attach to DataVista API",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
},
{
"name": "Chrome against http://localhost:5173",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/src/frontend"
}
],
"compounds": [
{ "name": "Backend + Frontend", "configurations": ["Launch DataVista API", "Launch Frontend (Vite)"], "stopAll": true },
{ "name": "Full stack + Browser", "configurations": ["Launch DataVista API", "Launch Frontend (Vite)", "Chrome against http://localhost:5173"], "stopAll": true }
]
}
.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{ "label": "restore-backend", "type": "process", "command": "dotnet", "args": ["restore","${workspaceFolder}/src/backend/DataVista.Backend.sln"] },
{ "label": "build-api", "type": "process", "command": "dotnet",
"args": ["build","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj","/property:GenerateFullPaths=true","/consoleloggerparameters:NoSummary"],
"problemMatcher": "$msCompile", "group": { "kind": "build", "isDefault": true }, "runOptions": { "reevaluateOnRerun": true } },
{ "label": "dev-frontend", "type": "shell", "command": "npm run dev", "options": { "cwd": "${workspaceFolder}/src/frontend" }, "isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated", "focus": false } },
{ "label": "build-frontend", "type": "shell", "command": "npm run build", "options": { "cwd": "${workspaceFolder}/src/frontend" }, "problemMatcher": ["$tsc"] },
{ "label": "typecheck-frontend", "type": "shell", "command": "npx tsc --noEmit", "options": { "cwd": "${workspaceFolder}/src/frontend" }, "problemMatcher": ["$tsc"], "group": "build" },
{ "label": "run-api", "type": "process", "command": "dotnet", "args": ["run","--project","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj"], "isBackground": true,
"presentation": { "reveal": "always", "panel": "dedicated", "clear": true } },
{ "label": "watch-api", "type": "process", "command": "dotnet", "args": ["watch","run","--project","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj"], "isBackground": true },
{ "label": "clean-api", "type": "process", "command": "dotnet", "args": ["clean","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj","/property:GenerateFullPaths=true","/consoleloggerparameters:NoSummary"] },
{ "label": "build-backend-solution", "type": "process", "command": "dotnet", "args": ["build","${workspaceFolder}/src/backend/DataVista.Backend.sln","/property:GenerateFullPaths=true","/consoleloggerparameters:NoSummary"] },
{ "label": "clean-backend-solution", "type": "process", "command": "dotnet", "args": ["clean","${workspaceFolder}/src/backend/DataVista.Backend.sln","/property:GenerateFullPaths=true","/consoleloggerparameters:NoSummary"] },
{ "label": "test-backend", "type": "process", "command": "dotnet", "args": ["test","${workspaceFolder}/src/backend/DataVista.Backend.sln","--nologo"], "group": "test" },
{ "label": "ef-migrations-add", "type": "process", "command": "dotnet",
"args": ["ef","migrations","add","${input:migrationName}","--project","${workspaceFolder}/src/backend/DataVista.Infrastructure/DataVista.Infrastructure.csproj","--startup-project","${workspaceFolder}/src/backend/DataVista.API/DataVista.API.csproj"],
"problemMatcher": "$msCompile" },
{ "label": "ef-migrations-script", "type": "process", "command": "dotnet",
"args": ["ef","migrations","script","${input:fromMigration}","${input:toMigration}","-o","${workspaceFolder}/artifacts/migration.sql"],
"problemMatcher": "$msCompile" },
{ "label": "start-services", "type": "shell", "command": "docker compose -f docker-compose.yml up -d postgres redis", "options": { "cwd": "${workspaceFolder}", "env": { "COMPOSE_PROFILES": "dev" } } },
{ "label": "stop-services", "type": "shell", "command": "docker compose down", "options": { "cwd": "${workspaceFolder}" } },
{ "label": "bootstrap", "dependsOrder": "sequence", "dependsOn": ["restore-backend","start-services","dev-frontend"] },
{ "label": "bundle-one-file", "type": "shell", "command": "esbuild ${file} --bundle --outfile=${workspaceFolder}/dist/${fileBasenameNoExtension}.js" }
],
"inputs": [
{ "id": "migrationName", "type": "promptString", "description": "Migration name", "default": "AddUsers" },
{ "id": "fromMigration", "type": "pickString", "description": "From migration", "options": ["0","Initial","Previous"] },
{ "id": "toMigration", "type": "promptString", "description": "To migration (blank = latest)", "default": "" }
]
}
From my experience, investing 15 minutes into these two files returns value every single day. Tune paths to your repo, commit them, and your whole team gets a predictable, one‑key workflow.