Shiny β NHS Quickstart
π§ͺ R Β· Interactive dashboards Β· SQL Server/Parquet Β· NHS-friendly hosting
Why Shiny in the NHS
If your team already uses R for stats and reporting, Shiny converts existing analysis into interactive apps with minimal extra code. Itβs ideal for internal dashboards where you want auditable R scripts, quick iteration, and secure, behind-SSO access.
Great for: Clinician-Researcher Β· BI Analyst (R-heavy) Β· Data Scientist.
βοΈ 10-minute installβ
In the R console:
install.packages(c("shiny","DBI","odbc","ggplot2","dotenv","readr","dplyr","arrow"))
- For SQL Server access, install the Microsoft ODBC Driver 18 for SQL Server for your OS.
- Keep secrets out of code; use .Renviron or the dotenv package locally and a secret store in production.
π βHello NHSβ Shiny app (choose a data source)β
- A) Parquet/CSV (safer pattern)
- B) SQL Server (direct)
- C) DuckDB (local analytics)
Folder layout
shiny-nhs/
.env
app.R
data/
kpi.parquet # or kpi.csv
.env (local only β never commit)
DATA_PATH=data/kpi.parquet
app.R
library(shiny)
library(ggplot2)
library(dplyr)
library(dotenv)
library(arrow) # for Parquet
load_dot_env()
data_path <- Sys.getenv("DATA_PATH", unset = "data/kpi.parquet")
df <- arrow::read_parquet(data_path)
ui <- fluidPage(
titlePanel("NHS KPI Dashboard (Shiny)"),
selectInput("top_n", "Top N practices", choices = c(5,10,20), selected = 10),
plotOutput("barPlot")
)
server <- function(input, output, session) {
output$barPlot <- renderPlot({
df |>
arrange(desc(total_appointments)) |>
head(as.numeric(input$top_n)) |>
ggplot(aes(x = reorder(practice_id, total_appointments),
y = total_appointments)) +
geom_col(fill = "#005EB8") +
coord_flip() +
labs(x = "Practice", y = "Appointments (30d)",
title = "Appointments by Practice") +
theme_minimal(base_size = 12)
})
}
shinyApp(ui, server)
Run in R:
shiny::runApp("shiny-nhs")
Folder layout
shiny-nhs/
.Renviron
app.R
.Renviron (local; or use dotenv)
SQLSERVER_SERVER=YOURSERVER
SQLSERVER_DATABASE=NHS_Analytics
app.R
library(shiny)
library(DBI)
library(odbc)
library(ggplot2)
library(dplyr)
con <- dbConnect(odbc::odbc(),
Driver = "ODBC Driver 18 for SQL Server",
Server = Sys.getenv("SQLSERVER_SERVER"),
Database = Sys.getenv("SQLSERVER_DATABASE"),
Trusted_Connection = "Yes",
Encrypt = "Yes",
TrustServerCertificate = "Yes" # set per Trust policy
)
onStop(function() dbDisconnect(con))
df <- dbGetQuery(con, "SELECT practice_id,total_appointments FROM dbo.vw_PracticeKPI")
ui <- fluidPage(
titlePanel("NHS KPI Dashboard (Shiny)"),
plotOutput("barPlot")
)
server <- function(input, output, session) {
output$barPlot <- renderPlot({
ggplot(df, aes(x = reorder(practice_id, total_appointments),
y = total_appointments)) +
geom_col(fill = "#005EB8") +
coord_flip() +
theme_minimal(base_size = 12)
})
}
shinyApp(ui, server)
Prefer read-only accounts and views; for production, consider extracting to Parquet and pointing the app at files to minimise live DB access.
Folder layout
shiny-nhs/
app.R
data/nhs.duckdb
app.R
library(shiny)
library(DBI)
library(duckdb)
library(dplyr)
library(ggplot2)
con <- dbConnect(duckdb::duckdb(), dbdir = "data/nhs.duckdb", read_only = TRUE)
onStop(function() dbDisconnect(con, shutdown = TRUE))
df <- dbGetQuery(con, "SELECT practice_id, COUNT(*) AS total_appointments
FROM appointments
GROUP BY practice_id")
ui <- fluidPage(
h3("NHS KPI (DuckDB)"),
plotOutput("chart")
)
server <- function(input, output, session){
output$chart <- renderPlot({
ggplot(df, aes(reorder(practice_id, total_appointments), total_appointments)) +
geom_col(fill = "#005EB8") + coord_flip() +
labs(x="Practice", y="Appointments")
})
}
shinyApp(ui, server)
π§° NHS-ready add-onsβ
- Branding: apply NHS.UK palette via
bslibor usenhsukdown. - Auth/SSO: place Shiny behind a reverse proxy (IIS/NGINX) integrated with Entra ID / NHS Login.
- Caching: cache expensive queries with
memoiseor precompute to Parquet on a schedule. - Validation: add a lightweight checks page (row counts, freshness) rendered from a validation script.
- Containerisation: package with Docker for consistent deploys (then run on App Runner / Azure Container Apps).
π’ Deploy optionsβ
- Posit Connect (formerly RStudio Connect) β easiest publishing, SSO integration options.
- Shiny Server Open Source β single box; put behind SSO at the edge.
- Docker + Cloud β containerise and deploy to AWS App Runner or Azure Container Apps; serve over HTTPS; restrict by IP/SSO.
- Intranet β host on a VM, fronted by IIS/NGINX with basic auth or integrated auth.
Document owner, refresh cadence, and datasets in the app README.
π IG & safety checklistβ
- Use synthetic/de-identified sample data for examples.
- Keep secrets in environment variables; never commit
.Renviron/.env. - Apply suppression for small numbers before plotting.
- Log access via reverse proxy and keep an audit trail of releases.
- Minimise live DB access; prefer read-only extracts for dashboards.
π Measuring impactβ
- Latency: data refresh β app reflects change.
- Stability: uptime and error rate.
- Quality: validation pass rate (row counts, freshness checks).
- Adoption: weekly active users; stakeholder satisfaction.
π See alsoβ
Whatβs next?
Youβve completed the Learn β Shiny stage. Keep momentum: