diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fcaf21746928f2d89b8fb7fc894e91499fea82d
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesGroundMotions.java
@@ -0,0 +1,77 @@
+package gov.usgs.earthquake.nshmp.netcdf;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
+
+/**
+ * Read in and parse all AASHTO NetCDF files for a particular year.
+ *
+ * @author U.S. Geological Survey
+ */
+public class NetcdfDataFilesGroundMotions extends NetcdfDataFiles<NetcdfGroundMotions> {
+
+  private int aashtoYear;
+
+  public NetcdfDataFilesGroundMotions(Path netcdfPath, int aashtoYear) {
+    super(netcdfPath, NetcdfDataType.GROUND_MOTIONS);
+    this.aashtoYear = aashtoYear;
+    addAll(readFiles(netcdfPath, dataType()));
+  }
+
+  /**
+   * Returns the aastho year of the NetCDF files.
+   */
+  public int aashtoYear() {
+    return aashtoYear;
+  }
+
+  /**
+   * Returns the {@link NetcdfGroundMotions} for a given site.
+   *
+   * @param location The site to get the NetCDF data
+   */
+  public NetcdfGroundMotions netcdf(Location location) {
+    return stream()
+        .filter(netcdf -> netcdf.netcdfData().contains(location))
+        .findFirst()
+        .orElseThrow(() -> new IllegalArgumentException(
+            String.format(
+                "Longitude %s and latitude %s not found in data sets.",
+                location.longitude,
+                location.latitude)));
+  }
+
+  @Override
+  protected List<NetcdfGroundMotions> readFiles(Path netcdfPath, NetcdfDataType dataType) {
+    try {
+      List<NetcdfGroundMotions> data = walkFiles(netcdfPath, dataType)
+          .map(NetcdfGroundMotions::new)
+          .collect(Collectors.toList());
+
+      if (data.isEmpty()) {
+        throw new FileNotFoundException(
+            String.format("Failed to find AASHTO %s NetCDF files", aashtoYear()));
+      }
+
+      return List.copyOf(data);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to read NetCDF directory " + netcdfPath, e);
+    }
+  }
+
+  @Override
+  protected Stream<Path> walkFiles(Path netcdfPath, NetcdfDataType dataType) throws IOException {
+    return super.walkFiles(netcdfPath, dataType)
+        .filter(file -> NetcdfUtils.readAttribute(
+            Key.YEAR,
+            file).getNumericValue().intValue() == aashtoYear());
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java
index 3a9c943b3b74a5f31ff24cbf626dcb54537f0359..d20109927ac0ea27d6dce4be0dc72e08d6579191 100644
--- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfGroundMotions.java
@@ -9,7 +9,7 @@ import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
 import gov.usgs.earthquake.nshmp.netcdf.data.BoundingData;
-import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfDataGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
 import gov.usgs.earthquake.nshmp.netcdf.reader.BoundingReaderGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.reader.ReaderGroundMotions;
@@ -33,8 +33,8 @@ public class NetcdfGroundMotions extends Netcdf<XySequence> {
   }
 
   @Override
-  public NetcdfData netcdfData() {
-    return netcdfData;
+  public NetcdfDataGroundMotions netcdfData() {
+    return (NetcdfDataGroundMotions) netcdfData;
   }
 
   @Override
@@ -56,7 +56,7 @@ public class NetcdfGroundMotions extends Netcdf<XySequence> {
       var group = ncd.getRootGroup();
       return new ReaderGroundMotions(group);
     } catch (IOException e) {
-      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + " ]");
+      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + "]");
     }
   }
 }
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d45dd2a3d9de063ff946db52976855de8c48e27
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataGroundMotions.java
@@ -0,0 +1,29 @@
+package gov.usgs.earthquake.nshmp.netcdf.data;
+
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.geo.Region;
+import gov.usgs.earthquake.nshmp.geo.Regions;
+
+/**
+ * NetCDF data container for ground motions (AASHTO) data.
+ *
+ * @author U.S. Geological Survey
+ */
+public class NetcdfDataGroundMotions extends NetcdfData {
+
+  private final Region region;
+
+  public NetcdfDataGroundMotions(NetcdfData netcdfData) {
+    super(NetcdfData.Builder.copyOf(netcdfData));
+    region = Regions.createRectangular("Region", minimumBounds(), maximumBounds());
+  }
+
+  /**
+   * Whether a location is contained in the bounds.
+   *
+   * @param location The location to test
+   */
+  public boolean contains(Location location) {
+    return region.contains(location);
+  }
+}
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java
index f45da139d717469be4fc52160892913a4d392736..716c3da4686aeb78d8a1f55318ce40484b599ebb 100644
--- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderGroundMotions.java
@@ -2,6 +2,8 @@ package gov.usgs.earthquake.nshmp.netcdf.reader;
 
 import java.io.IOException;
 
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfDataGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape;
 import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape.IndexKey;
 import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
@@ -29,4 +31,10 @@ public class ReaderGroundMotions extends Reader {
         .add(IndexKey.SITE_CLASS, vData.findDimensionIndex(Key.SITE_CLASS))
         .build();
   }
+
+  @Override
+  NetcdfDataGroundMotions readData(Group targetGroup) throws IOException {
+    NetcdfData netcdfData = super.readData(targetGroup);
+    return new NetcdfDataGroundMotions(netcdfData);
+  }
 }
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
index bb5bc19870dadd7c7fb864fcc397ef05489106fd..6c204a6e4d628ee365812abd79e3b6a433899d77 100644
--- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
@@ -3,7 +3,7 @@ package gov.usgs.earthquake.nshmp.netcdf.www;
 import java.nio.file.Path;
 
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
 import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
@@ -43,9 +43,12 @@ public class NetcdfController {
   @Inject
   private NshmpMicronautServlet servlet;
 
-  @Value("${nshmp-ws-static.netcdf-file}")
+  @Value("${nshmp-ws-static.netcdf-path}")
   Path netcdfPath;
 
+  @Value("${nshmp-ws-static.aashto-year}")
+  int aashtoYear;
+
   NetcdfServiceGroundMotions service;
 
   /**
@@ -53,8 +56,8 @@ public class NetcdfController {
    */
   @EventListener
   void startup(StartupEvent event) {
-    var netcdf = new NetcdfGroundMotions(netcdfPath);
-    service = new NetcdfServiceGroundMotions(netcdf);
+    var netcdfDataFiles = new NetcdfDataFilesGroundMotions(netcdfPath, aashtoYear);
+    service = new NetcdfServiceGroundMotions(netcdfDataFiles);
   }
 
   /**
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java
index e5ddcae46b8b3fa7c96fc3f7fb12ce30ecb06804..3ac06ee66e8e4892f20c5a6af90c003e2d4ccff2 100644
--- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceGroundMotions.java
@@ -5,10 +5,12 @@ import java.util.stream.Collectors;
 
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
 import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion;
 import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
 import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata;
+import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.SourceModel;
 import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key;
 import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData;
@@ -26,21 +28,22 @@ import io.micronaut.http.HttpRequest;
  *
  * @author U.S. Geological Survey
  */
-public class NetcdfServiceGroundMotions extends NetcdfService<Query> {
+public class NetcdfServiceGroundMotions extends NetcdfService<List<SourceModel>, Query> {
 
   static final String SERVICE_DESCRIPTION = "Get static ground motions from a NetCDF file";
   static final String X_LABEL = "Period (s)";
   static final String Y_LABEL = "Spectral Acceleration (g)";
 
-  public NetcdfServiceGroundMotions(NetcdfGroundMotions netcdf) {
-    super(netcdf);
+  public NetcdfServiceGroundMotions(NetcdfDataFilesGroundMotions netcdfDataFiles) {
+    super(netcdfDataFiles);
   }
 
   @Override
-  ResponseBody<String, Metadata<Query>> getMetadataResponse(HttpRequest<?> request) {
-    var metadata = new Metadata<Query>(request, this, SERVICE_DESCRIPTION);
+  ResponseBody<String, Metadata<List<SourceModel>, Query>> getMetadataResponse(
+      HttpRequest<?> request) {
+    var metadata = new Metadata<List<SourceModel>, Query>(request, this, SERVICE_DESCRIPTION);
 
-    return ResponseBody.<String, Metadata<Query>> usage()
+    return ResponseBody.<String, Metadata<List<SourceModel>, Query>> usage()
         .metadata(new ResponseMetadata(NetcdfVersion.appVersions()))
         .name(getServiceName())
         .request(NetcdfWsUtils.getRequestUrl(request))
@@ -49,19 +52,24 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> {
         .build();
   }
 
+  List<SourceModel> getSourceModels() {
+    return netcdfDataFiles().stream()
+        .map(SourceModel::new)
+        .collect(Collectors.toList());
+  }
+
   @Override
   String getServiceName() {
-    return getSourceModel().name;
+    return String.format("AASHTO-%d Web Services", netcdfDataFiles().aashtoYear());
   }
 
-  @Override
-  SourceModel getSourceModel() {
-    return new SourceModel(netcdf());
+  NetcdfGroundMotions netcdf(Location location) {
+    return netcdfDataFiles().netcdf(location);
   }
 
   @Override
-  NetcdfGroundMotions netcdf() {
-    return (NetcdfGroundMotions) netcdf;
+  NetcdfDataFilesGroundMotions netcdfDataFiles() {
+    return (NetcdfDataFilesGroundMotions) netcdfDataFiles;
   }
 
   @Override
@@ -82,14 +90,13 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> {
     }
   }
 
-  @Override
   ResponseBody<RequestDataSiteClass, ResponseData<ServiceResponseMetadata>> processCurvesSiteClass(
       RequestDataSiteClass request,
       String url) {
     WsUtils.checkValue(Key.LATITUDE, request.latitude);
     WsUtils.checkValue(Key.LONGITUDE, request.longitude);
     WsUtils.checkValue(Key.SITE_CLASS, request.siteClass);
-    var curves = netcdf().staticData(request.site, request.siteClass);
+    var curves = netcdf(request.site).staticData(request.site, request.siteClass);
     var responseData = toResponseData(request, curves);
 
     return ResponseBody.<RequestDataSiteClass, ResponseData<ServiceResponseMetadata>> success()
@@ -101,13 +108,12 @@ public class NetcdfServiceGroundMotions extends NetcdfService<Query> {
         .build();
   }
 
-  @Override
   ResponseBody<RequestData, List<ResponseData<ServiceResponseMetadata>>> processCurves(
       RequestData request,
       String url) {
     WsUtils.checkValue(Key.LATITUDE, request.latitude);
     WsUtils.checkValue(Key.LONGITUDE, request.longitude);
-    var curves = netcdf().staticData(request.site);
+    var curves = netcdf(request.site).staticData(request.site);
     var responseData = toList(request, curves);
 
     return ResponseBody.<RequestData, List<ResponseData<ServiceResponseMetadata>>> success()
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
index 5bd776c0040d70de43e8e21763d8533b1170b53d..727142bac7070531ce86f38c96a1f6046f8adef5 100644
--- a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
@@ -1,9 +1,8 @@
 package gov.usgs.earthquake.nshmp.netcdf.www;
 
-import java.io.IOException;
 import java.nio.file.Path;
 
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfGroundMotions;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesGroundMotions;
 import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
 
 import io.micronaut.context.annotation.Value;
@@ -34,74 +33,30 @@ public class SwaggerController {
   @Inject
   private NshmpMicronautServlet servlet;
 
-  @Value("${nshmp-ws-static.netcdf-file}")
+  @Value("${nshmp-ws-static.netcdf-path}")
   Path netcdfPath;
 
-  NetcdfGroundMotions netcdf;
+  @Value("${nshmp-ws-static.aashto-year}")
+  int aashtoYear;
+
+  NetcdfServiceGroundMotions service;
 
   /**
    * Read in data type and return the appropriate service to use.
    */
   @EventListener
   void startup(StartupEvent event) {
-    netcdf = new NetcdfGroundMotions(netcdfPath);
+    var netcdfDataFiles = new NetcdfDataFilesGroundMotions(netcdfPath, aashtoYear);
+    service = new NetcdfServiceGroundMotions(netcdfDataFiles);
   }
 
   @Get(produces = MediaType.TEXT_EVENT_STREAM)
   public HttpResponse<String> doGet(HttpRequest<?> request) {
     try {
-      var openApi = NetcdfWsUtils.getOpenApi(
-          request,
-          netcdf.netcdfData(),
-          servicePatternSection(request));
-      return HttpResponse.ok(Yaml.pretty(openApi));
+      SwaggerGroundMotions swagger = new SwaggerGroundMotions(request, service);
+      return HttpResponse.ok(Yaml.pretty(swagger.updateOpenApi()));
     } catch (Exception e) {
       return NetcdfWsUtils.handleError(e, "Swagger", request.getUri().getPath());
     }
   }
-
-  private static String servicePatternSection(HttpRequest<?> request)
-      throws IOException {
-    var url = NetcdfWsUtils.getRequestUrl(request);
-    url = url.endsWith("/swagger") ? url.replace("/swagger", "") : url;
-
-    return new StringBuilder()
-        .append(
-            "<details>\n" +
-                "<summary>Service Call Patterns</summary>\n")
-        .append(
-            "### Query Pattern\n" +
-
-                "The query based service call is in the form of:\n" +
-
-                "```text\n" +
-                url + "/spectra?longitude={number}&latitude={number}\n" +
-                url + "/spectra?longitude={number}&latitude={number}&siteClass={string}\n" +
-                "````\n" +
-
-                "Example:\n" +
-                "```text\n" +
-                url + "/spectra?longitude=-118&latitude=34\n" +
-                url + "/spectra?longitude=-118&latitude=34&siteClass=BC\n" +
-                "```\n")
-        .append(
-            "### Slash Pattern\n" +
-
-                "The slash based service call is in the form of:\n" +
-
-                "```text\n" +
-                url + "/spectra/{longitude}/{latitude}\n" +
-                url + "/spectra/{longitude}/{latitude}/{siteClass}\n" +
-                "```\n" +
-
-                "Example:\n" +
-
-                "```text\n" +
-                url + "/spectra/-118/34\n" +
-                url + "/spectra/-118/34/BC\n" +
-                "```\n")
-        .append("</details>")
-        .toString();
-  }
-
 }
diff --git a/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerGroundMotions.java b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerGroundMotions.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae1bf7db22fff06c624e1b46a8cacdfa0aa9db35
--- /dev/null
+++ b/src/aashto/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerGroundMotions.java
@@ -0,0 +1,94 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
+
+import io.micronaut.http.HttpRequest;
+
+/**
+ * Swagger page updates for ground motion services.
+ *
+ * @author U.S. Geological Survey
+ */
+class SwaggerGroundMotions extends Swagger<NetcdfServiceGroundMotions> {
+
+  SwaggerGroundMotions(HttpRequest<?> request, NetcdfServiceGroundMotions service) {
+    super(request, service);
+  }
+
+  @Override
+  String description() {
+    StringBuilder builder = new StringBuilder()
+        .append(serviceInfo())
+        .append("\n## " + descriptionHeader() + "\n");
+
+    service.netcdfDataFiles()
+        .forEach(netcdf -> {
+          ScienceBaseMetadata metadata = netcdf.netcdfData().scienceBaseMetadata();
+          builder
+              .append("### " + metadata.label + "\n")
+              .append(metadata.description + "\n")
+              .append(parameterSection(netcdf.netcdfData()) + "\n")
+              .append(scienceBaseSection(metadata) + "\n");
+        });
+
+    return builder.toString();
+  }
+
+  @Override
+  String descriptionHeader() {
+    return String.format("AASHTO-%d Data Sets", service.netcdfDataFiles().aashtoYear());
+  }
+
+  @Override
+  String serviceInfo() {
+    return String.format(String.join("",
+        "Get risk-targeted design response spectra for the %d ",
+        "editions of American Association of State Highway and Transportation ",
+        "Officials (AASHTO) bridge design specifications."),
+        service.netcdfDataFiles().aashtoYear());
+  }
+
+  @Override
+  String servicePatternSection() {
+    var url = NetcdfWsUtils.getRequestUrl(request);
+    url = url.endsWith("/swagger") ? url.replace("/swagger", "") : url;
+
+    return new StringBuilder()
+        .append(
+            "<details>\n" +
+                "<summary>Service Call Patterns</summary>\n")
+        .append(
+            "### Query Pattern\n" +
+
+                "The query based service call is in the form of:\n" +
+
+                "```text\n" +
+                url + "/spectra?longitude={number}&latitude={number}\n" +
+                url + "/spectra?longitude={number}&latitude={number}&siteClass={string}\n" +
+                "````\n" +
+
+                "Example:\n" +
+                "```text\n" +
+                url + "/spectra?longitude=-118&latitude=34\n" +
+                url + "/spectra?longitude=-118&latitude=34&siteClass=BC\n" +
+                "```\n")
+        .append(
+            "### Slash Pattern\n" +
+
+                "The slash based service call is in the form of:\n" +
+
+                "```text\n" +
+                url + "/spectra/{longitude}/{latitude}\n" +
+                url + "/spectra/{longitude}/{latitude}/{siteClass}\n" +
+                "```\n" +
+
+                "Example:\n" +
+
+                "```text\n" +
+                url + "/spectra/-118/34\n" +
+                url + "/spectra/-118/34/BC\n" +
+                "```\n")
+        .append("</details>")
+        .toString();
+  }
+}
diff --git a/src/aashto/src/main/resources/aashto-example.nc b/src/aashto/src/main/resources/aashto-example.nc
index 799953a05255d3a6a4918ec11f36372a552a49c7..125a3d19c9b36ed44667ee08d97c8603dc040611 100644
Binary files a/src/aashto/src/main/resources/aashto-example.nc and b/src/aashto/src/main/resources/aashto-example.nc differ
diff --git a/src/aashto/src/main/resources/application.yml b/src/aashto/src/main/resources/application.yml
index 70d77ecea5826d7592f8c02b83a0eac4b028e1da..2b1921f1539347e1075f8905bce8e8ec1ab430b4 100644
--- a/src/aashto/src/main/resources/application.yml
+++ b/src/aashto/src/main/resources/application.yml
@@ -16,4 +16,7 @@ micronaut:
         logger-name: http
 
 nshmp-ws-static:
-  netcdf-file: ${netcdf:src/main/resources/aashto-example.nc}
+  # AASHTO year to filter NetCDF files by
+  aashto-year: ${year:2023}
+  # Path to directory holding NetCDF files
+  netcdf-path: ${netcdf:src/main/resources}
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java
new file mode 100644
index 0000000000000000000000000000000000000000..c429b83ced424369a79c7afcb1cf62254e1054b4
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFilesHazardCurves.java
@@ -0,0 +1,49 @@
+package gov.usgs.earthquake.nshmp.netcdf;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+
+/**
+ * Read in and parse a single hazard NetCDF data file.
+ *
+ * @author U.S. Geological Survey
+ */
+public class NetcdfDataFilesHazardCurves extends NetcdfDataFiles<NetcdfHazardCurves> {
+
+  public NetcdfDataFilesHazardCurves(Path netcdfPath) {
+    super(netcdfPath, NetcdfDataType.HAZARD_CURVES);
+    addAll(readFiles(netcdfPath, dataType()));
+  }
+
+  /**
+   * Returns the set of IMTs.
+   */
+  public Set<Imt> imts() {
+    return Set.copyOf(netcdf().netcdfData().imts());
+  }
+
+  /**
+   * Returns the Netcdf object.
+   */
+  public NetcdfHazardCurves netcdf() {
+    return stream()
+        .findFirst()
+        .orElseThrow(() -> new IllegalArgumentException("Data set not found"));
+  }
+
+  @Override
+  protected List<NetcdfHazardCurves> readFiles(Path netcdfPath, NetcdfDataType dataType) {
+    NetcdfDataType fileDataType = NetcdfDataType.getDataType((netcdfPath));
+
+    if (!fileDataType.equals(dataType)) {
+      throw new RuntimeException(
+          String.format("Data type %s of file must be of type %s", fileDataType, dataType));
+    } ;
+
+    NetcdfHazardCurves data = new NetcdfHazardCurves(netcdfPath);
+    return List.of(data);
+  }
+}
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfHazardCurves.java
index 4fedc864f22f334c6599266d233e791a589b3003..3f6aa549e01d79fbc318a2777dd24340c7328023 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfHazardCurves.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfHazardCurves.java
@@ -56,7 +56,7 @@ public class NetcdfHazardCurves extends Netcdf<StaticDataHazardCurves> {
       var group = ncd.getRootGroup();
       return new ReaderHazardCurves(group);
     } catch (IOException e) {
-      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + " ]");
+      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + "]");
     }
   }
 }
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataHazardCurves.java
index af9df24c6ad72820f09624a47ffae0dd24f9c11d..e144ae5476e0a74c35c0af382651830fdc5c8f8d 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataHazardCurves.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfDataHazardCurves.java
@@ -2,11 +2,9 @@ package gov.usgs.earthquake.nshmp.netcdf.data;
 
 import static com.google.common.base.Preconditions.checkState;
 
-import java.util.List;
 import java.util.Map;
 
 import gov.usgs.earthquake.nshmp.gmm.Imt;
-import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
 
 /**
  * NetCDF data for hazard curves.
@@ -17,9 +15,10 @@ public class NetcdfDataHazardCurves extends NetcdfData {
 
   private final Map<Imt, double[]> imls;
 
-  NetcdfDataHazardCurves(Builder builder) {
-    super(builder);
-    imls = builder.imls;
+  public NetcdfDataHazardCurves(NetcdfData netcdfData, Map<Imt, double[]> imls) {
+    super(NetcdfData.Builder.copyOf(netcdfData));
+    checkState(!imls.isEmpty(), "Must add imls");
+    this.imls = imls;
   }
 
   /**
@@ -39,63 +38,4 @@ public class NetcdfDataHazardCurves extends NetcdfData {
   public static Builder builder() {
     return new Builder();
   }
-
-  public static class Builder extends NetcdfData.Builder {
-    Map<Imt, double[]> imls;
-
-    Builder() {
-      super();
-    }
-
-    public Builder imls(Map<Imt, double[]> imls) {
-      this.imls = imls;
-      return this;
-    }
-
-    @Override
-    public Builder imts(List<Imt> imts) {
-      super.imts(imts);
-      return this;
-    }
-
-    @Override
-    public Builder latitudes(double[] latitudes) {
-      super.latitudes(latitudes);
-      return this;
-    }
-
-    @Override
-    public Builder longitudes(double[] longitudes) {
-      super.longitudes(longitudes);
-      return this;
-    }
-
-    @Override
-    public Builder scienceBaseMetadata(ScienceBaseMetadata scienceBaseMetadata) {
-      super.scienceBaseMetadata(scienceBaseMetadata);
-      return this;
-    }
-
-    @Override
-    public Builder siteClasses(List<NehrpSiteClass> siteClasses) {
-      super.siteClasses(siteClasses);
-      return this;
-    }
-
-    @Override
-    public Builder vs30Map(Map<NehrpSiteClass, Double> vs30Map) {
-      super.vs30Map(vs30Map);
-      return this;
-    }
-
-    public NetcdfDataHazardCurves build() {
-      checkBuildState();
-      return new NetcdfDataHazardCurves(this);
-    }
-
-    void checkBuildState() {
-      super.checkBuildState();
-      checkState(!imls.isEmpty(), "Must add imls");
-    }
-  }
 }
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderHazardCurves.java
index b90c086be3d7e1cbe9756a2c69b2d58885daea6c..f8531d56ae5533bae0695d3d657f2569f8305b8a 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderHazardCurves.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/ReaderHazardCurves.java
@@ -8,6 +8,7 @@ import java.util.List;
 import java.util.Map;
 
 import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
 import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfDataHazardCurves;
 import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape;
 import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfShape.IndexKey;
@@ -36,21 +37,13 @@ public class ReaderHazardCurves extends Reader {
 
   @Override
   NetcdfDataHazardCurves readData(Group targetGroup) throws IOException {
-    var coords = super.readData(targetGroup);
+    NetcdfData netcdfData = super.readData(targetGroup);
     var vImls = targetGroup.findVariableLocal(Key.IMLS);
 
     // get map of IMLs
-    var imls = mapImls(vImls, coords.imts());
+    var imls = mapImls(vImls, netcdfData.imts());
 
-    return NetcdfDataHazardCurves.builder()
-        .imls(imls)
-        .imts(coords.imts())
-        .latitudes(coords.latitudes())
-        .longitudes(coords.longitudes())
-        .scienceBaseMetadata(coords.scienceBaseMetadata())
-        .siteClasses(coords.siteClasses())
-        .vs30Map(coords.vs30Map())
-        .build();
+    return new NetcdfDataHazardCurves(netcdfData, imls);
   }
 
   @Override
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardMetadata.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardMetadata.java
new file mode 100644
index 0000000000000000000000000000000000000000..fe7c8b3683284fed828871d46ff834019ce32ee0
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/HazardMetadata.java
@@ -0,0 +1,33 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata;
+import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData;
+import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestDataImt;
+
+/**
+ * Hazard metadata.
+ *
+ * @author U.S. Geological Survey
+ */
+public class HazardMetadata {
+
+  static class HazardResponseMetadata extends ServiceResponseMetadata {
+    public final Imt imt;
+
+    HazardResponseMetadata(
+        NehrpSiteClass siteClass,
+        Imt imt,
+        String xLabel,
+        String yLabel) {
+      super(siteClass, xLabel, yLabel);
+      this.imt = imt;
+    }
+
+    @Override
+    public HazardRequestDataImt toRequestMetadata(RequestData requestData) {
+      return new HazardRequestDataImt(requestData.site, siteClass, imt, requestData.format);
+    }
+  }
+}
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
index f14d8449a2aa35055936926c95369321a10154d7..4e1a688ead05ae7f864cfa4c981a7ad41204a2cb 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfController.java
@@ -5,10 +5,10 @@ import java.util.List;
 
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves;
-import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.HazardResponseMetadata;
-import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesHazardCurves;
+import gov.usgs.earthquake.nshmp.netcdf.www.HazardMetadata.HazardResponseMetadata;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestDataImt;
 import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
 
@@ -32,8 +32,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.inject.Inject;
 
 /**
- * Micronaut controller for getting static hazards or ground motions from a
- * NetCDF file.
+ * Micronaut controller for getting static hazards from a NetCDF file.
  *
  * @see NetcdfService
  *
@@ -46,7 +45,10 @@ public class NetcdfController {
   @Inject
   private NshmpMicronautServlet servlet;
 
-  @Value("${nshmp-ws-static.netcdf-file}")
+  /**
+   * The path to a hazard NetCDF file
+   */
+  @Value("${nshmp-ws-static.netcdf-path}")
   Path netcdfPath;
 
   NetcdfServiceHazardCurves service;
@@ -56,8 +58,8 @@ public class NetcdfController {
    */
   @EventListener
   void startup(StartupEvent event) {
-    var netcdfHazard = new NetcdfHazardCurves(netcdfPath);
-    service = new NetcdfServiceHazardCurves(netcdfHazard);
+    var netcdfDataFiles = new NetcdfDataFilesHazardCurves(netcdfPath);
+    service = new NetcdfServiceHazardCurves(netcdfDataFiles);
   }
 
   /**
@@ -174,7 +176,8 @@ public class NetcdfController {
                       "..."))
 
       })
-  @Get(uri = "/{longitude}/{latitude}/{siteClass}{?format}", produces = MediaType.APPLICATION_JSON)
+  @Get(uri = "/{longitude}/{latitude}/{siteClass}{?format}",
+      produces = MediaType.APPLICATION_JSON)
   public HttpResponse<String> doGetSlashBySite(
       HttpRequest<?> request,
       @Schema(required = true) @PathVariable Double latitude,
@@ -249,6 +252,7 @@ public class NetcdfController {
    * GET method to return a static curve using URL query.
    *
    * @param request The HTTP request
+   * @param nshm The NSHM to query
    * @param longitude The longitude of the site
    * @param latitude Latitude of the site
    * @param siteClass The site class (optional)
@@ -298,7 +302,8 @@ public class NetcdfController {
                       "..."))
 
       })
-  @Get(uri = "{?longitude,latitude,siteClass,imt,format}", produces = MediaType.APPLICATION_JSON)
+  @Get(uri = "{?longitude,latitude,siteClass,imt,format}",
+      produces = MediaType.APPLICATION_JSON)
   public HttpResponse<String> doGet(
       HttpRequest<?> request,
       @Schema(required = true) @QueryValue @Nullable Double longitude,
@@ -314,7 +319,7 @@ public class NetcdfController {
   // For Swagger schema
   private static class ResponseByImt
       extends
-      ResponseBody<RequestDataImt, ResponseData<HazardResponseMetadata>> {}
+      ResponseBody<HazardRequestDataImt, ResponseData<HazardResponseMetadata>> {}
 
   // For Swagger schema
   private static class ResponseBySiteClass
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
index 831ccfc27c2c2ef6ee691fd0fe6891191bf463fe..6984e675bb8bf2b8232500c46e977c2a94c723e7 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfServiceHazardCurves.java
@@ -6,16 +6,18 @@ import java.util.stream.Collectors;
 import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesHazardCurves;
 import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves;
 import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion;
 import gov.usgs.earthquake.nshmp.netcdf.data.StaticData;
 import gov.usgs.earthquake.nshmp.netcdf.data.StaticDataHazardCurves;
-import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.HazardResponseMetadata;
+import gov.usgs.earthquake.nshmp.netcdf.www.HazardMetadata.HazardResponseMetadata;
+import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.SourceModel;
 import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key;
 import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData;
-import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.www.RequestHazardCurves.HazardRequestDataImt;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
 import gov.usgs.earthquake.nshmp.www.ResponseMetadata;
 import gov.usgs.earthquake.nshmp.www.WsUtils;
@@ -29,20 +31,22 @@ import io.micronaut.http.HttpRequest;
  *
  * @author U.S. Geological Survey
  */
-public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
+public class NetcdfServiceHazardCurves extends
+    NetcdfService<NetcdfServiceHazardCurves.HazardSourceModel, HazardQuery> {
 
   static final String SERVICE_DESCRIPTION = "Get static hazard curves from a NetCDF file";
   static final String X_LABEL = "Ground Motion (g)";
   static final String Y_LABEL = "Annual Frequency of Exceedence";
 
-  public NetcdfServiceHazardCurves(NetcdfHazardCurves netcdf) {
-    super(netcdf);
+  public NetcdfServiceHazardCurves(NetcdfDataFilesHazardCurves netcdfDataFiles) {
+    super(netcdfDataFiles);
   }
 
   @Override
-  ResponseBody<String, Metadata<HazardQuery>> getMetadataResponse(HttpRequest<?> request) {
-    var metadata = new Metadata<HazardQuery>(request, this, SERVICE_DESCRIPTION);
-    return ResponseBody.<String, Metadata<HazardQuery>> usage()
+  ResponseBody<String, Metadata<HazardSourceModel, HazardQuery>> getMetadataResponse(
+      HttpRequest<?> request) {
+    var metadata = new Metadata<HazardSourceModel, HazardQuery>(request, this, SERVICE_DESCRIPTION);
+    return ResponseBody.<String, Metadata<HazardSourceModel, HazardQuery>> usage()
         .metadata(new ResponseMetadata(NetcdfVersion.appVersions()))
         .name(getServiceName())
         .request(NetcdfWsUtils.getRequestUrl(request))
@@ -64,20 +68,29 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
 
   @Override
   String getServiceName() {
-    return getSourceModel().name;
+    return netcdfDataFiles().netcdf().netcdfData().scienceBaseMetadata().label;
   }
 
   @Override
-  SourceModel getSourceModel() {
-    return new HazardSourceModel();
+  HazardSourceModel getSourceModels() {
+    return new HazardSourceModel(netcdfDataFiles().netcdf());
   }
 
-  @Override
+  /**
+   * Returns the {@link NetcdfHazardCurves} associated with a given {@link Nshm
+   * NSHM}.
+   *
+   * @param nshm The NSHM to get the NetCDF data
+   */
   NetcdfHazardCurves netcdf() {
-    return (NetcdfHazardCurves) netcdf;
+    return netcdfDataFiles().netcdf();
   }
 
   @Override
+  NetcdfDataFilesHazardCurves netcdfDataFiles() {
+    return (NetcdfDataFilesHazardCurves) netcdfDataFiles;
+  }
+
   ResponseBody<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> processCurves(
       RequestData request,
       String url) {
@@ -86,7 +99,8 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
     var curves = netcdf().staticData(request.site);
     var curvesAsList = toList(request, curves);
 
-    return ResponseBody.<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> success()
+    return ResponseBody
+        .<RequestData, List<List<ResponseData<HazardResponseMetadata>>>> success()
         .metadata(new ResponseMetadata(NetcdfVersion.appVersions()))
         .name(getServiceName())
         .request(request)
@@ -95,7 +109,6 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
         .build();
   }
 
-  @Override
   ResponseBody<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> processCurvesSiteClass(
       RequestDataSiteClass request,
       String url) {
@@ -105,7 +118,8 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
     var curves = netcdf().staticData(request.site, request.siteClass);
     var curvesAsList = toList(request, curves);
 
-    return ResponseBody.<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> success()
+    return ResponseBody
+        .<RequestDataSiteClass, List<ResponseData<HazardResponseMetadata>>> success()
         .metadata(new ResponseMetadata(NetcdfVersion.appVersions()))
         .name(getServiceName())
         .request(request)
@@ -115,7 +129,7 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
   }
 
   ResponseBody<RequestDataSiteClass, ResponseData<HazardResponseMetadata>> processCurvesImt(
-      RequestDataImt request,
+      HazardRequestDataImt request,
       String url) {
     WsUtils.checkValue(Key.LATITUDE, request.latitude);
     WsUtils.checkValue(Key.LONGITUDE, request.longitude);
@@ -143,10 +157,12 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
         var requestData = new RequestData(site, query.format);
         return toResponseFromListOfList(processCurves(requestData, url));
       case CURVES_BY_SITE_CLASS:
-        var requestDataSiteClass = new RequestDataSiteClass(site, query.siteClass, query.format);
+        var requestDataSiteClass =
+            new RequestDataSiteClass(site, query.siteClass, query.format);
         return toResponseFromList(processCurvesSiteClass(requestDataSiteClass, url));
       case CURVES_BY_IMT:
-        var requestDataImt = new RequestDataImt(site, query.siteClass, query.imt, query.format);
+        var requestDataImt =
+            new HazardRequestDataImt(site, query.siteClass, query.imt, query.format);
         return toResponse(processCurvesImt(requestDataImt, url));
       default:
         throw new RuntimeException("Netcdf service [" + service + "] not found");
@@ -184,7 +200,8 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
     return curves.entrySet().stream()
         .map(entry -> {
           var request =
-              new RequestDataSiteClass(requestData.site, entry.getKey(), requestData.format);
+              new RequestDataSiteClass(requestData.site, entry.getKey(),
+                  requestData.format);
           return toList(request, entry.getValue());
         })
         .collect(Collectors.toList());
@@ -199,12 +216,12 @@ public class NetcdfServiceHazardCurves extends NetcdfService<HazardQuery> {
     return new ResponseData<>(metadata, curves);
   }
 
-  class HazardSourceModel extends SourceModel {
+  static class HazardSourceModel extends SourceModel {
     public final List<Imt> imts;
 
-    HazardSourceModel() {
-      super(netcdf());
-      imts = netcdf().netcdfData().imts().stream().sorted().collect(Collectors.toList());
+    HazardSourceModel(NetcdfHazardCurves netcdf) {
+      super(netcdf);
+      imts = netcdf.netcdfData().imts().stream().sorted().collect(Collectors.toList());
     }
   }
 }
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/RequestHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/RequestHazardCurves.java
new file mode 100644
index 0000000000000000000000000000000000000000..1332d2cbd156dc224a655a78856b75bd9035c321
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/RequestHazardCurves.java
@@ -0,0 +1,41 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.util.List;
+
+import gov.usgs.earthquake.nshmp.Text;
+import gov.usgs.earthquake.nshmp.Text.Delimiter;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.gmm.Imt;
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
+import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
+
+/**
+ * Request data for hazard services.
+ *
+ * @author U.S. Geological Survey
+ */
+public class RequestHazardCurves {
+
+  /**
+   * Request data with site class and imt
+   */
+  static class HazardRequestDataImt extends RequestDataSiteClass {
+    public Imt imt;
+
+    HazardRequestDataImt(
+        Location site,
+        NehrpSiteClass siteClass,
+        Imt imt,
+        ResponseFormat format) {
+      super(site, siteClass, format);
+      this.imt = imt;
+    }
+
+    @Override
+    public String toCsv() {
+      return String.format("%s,%s",
+          super.toCsv(), Text.join(List.of(Key.IMT.toString(), imt.name()), Delimiter.COMMA));
+    }
+  }
+}
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
index ac31adec41786e82aabe251c57e2f0fa945abfc6..40b1f2714a7b2360df33b50ab22241ba65a90987 100644
--- a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerController.java
@@ -1,11 +1,9 @@
 package gov.usgs.earthquake.nshmp.netcdf.www;
 
-import java.io.IOException;
 import java.nio.file.Path;
 
-import gov.usgs.earthquake.nshmp.netcdf.NetcdfHazardCurves;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFilesHazardCurves;
 import gov.usgs.earthquake.nshmp.www.NshmpMicronautServlet;
-import gov.usgs.earthquake.nshmp.www.SwaggerUtils;
 
 import io.micronaut.context.annotation.Value;
 import io.micronaut.context.event.StartupEvent;
@@ -35,79 +33,27 @@ public class SwaggerController {
   @Inject
   private NshmpMicronautServlet servlet;
 
-  @Value("${nshmp-ws-static.netcdf-file}")
+  @Value("${nshmp-ws-static.netcdf-path}")
   Path netcdfPath;
 
-  NetcdfHazardCurves netcdf;
+  NetcdfServiceHazardCurves service;
 
   /**
    * Read in data type and return the appropriate service to use.
    */
   @EventListener
   void startup(StartupEvent event) {
-    netcdf = new NetcdfHazardCurves(netcdfPath);
+    var netcdfDataFiles = new NetcdfDataFilesHazardCurves(netcdfPath);
+    service = new NetcdfServiceHazardCurves(netcdfDataFiles);
   }
 
   @Get(produces = MediaType.TEXT_EVENT_STREAM)
   public HttpResponse<String> doGet(HttpRequest<?> request) {
     try {
-      var openApi = NetcdfWsUtils.getOpenApi(
-          request,
-          netcdf.netcdfData(),
-          servicePatternSection(request));
-      SwaggerUtils.imtSchema(openApi.getComponents().getSchemas(), netcdf.netcdfData().imts());
-      return HttpResponse.ok(Yaml.pretty(openApi));
+      SwaggerHazardCurves swagger = new SwaggerHazardCurves(request, service);
+      return HttpResponse.ok(Yaml.pretty(swagger.updateOpenApi()));
     } catch (Exception e) {
       return NetcdfWsUtils.handleError(e, "Swagger", request.getUri().getPath());
     }
   }
-
-  private static String servicePatternSection(HttpRequest<?> request)
-      throws IOException {
-    var url = NetcdfWsUtils.getRequestUrl(request);
-    url = url.endsWith("/swagger") ? url.replace("/swagger", "") : url;
-
-    return new StringBuilder()
-        .append(
-            "<details>\n" +
-                "<summary>Service Call Patterns</summary>\n")
-        .append(
-            "### Query Pattern\n" +
-
-                "The query based service call is in the form of:\n" +
-
-                "```text\n" +
-                url + "/hazard?longitude={number}&latitude={number}\n" +
-                url + "/hazard?longitude={number}&latitude={number}&siteClass={string}\n" +
-                url +
-                "/hazard?longitude={number}&latitude={number}&siteClass={string}&imt={string}\n" +
-                "````\n" +
-
-                "Example:\n" +
-                "```text\n" +
-                url + "/hazard?longitude=-118&latitude=34\n" +
-                url + "/hazard?longitude=-118&latitude=34&siteClass=BC\n" +
-                url + "/hazard?longitude=-118&latitude=34&siteClass=BC&imt=PGA\n" +
-                "```\n")
-        .append(
-            "### Slash Pattern\n" +
-
-                "The slash based service call is in the form of:\n" +
-
-                "```text\n" +
-                url + "/hazard/{longitude}/{latitude}\n" +
-                url + "/hazard/{longitude}/{latitude}/{siteClass}\n" +
-                url + "/hazard/{longitude}/{latitude}/{siteClass}/{imt}\n" +
-                "```\n" +
-
-                "Example:\n" +
-
-                "```text\n" +
-                url + "/hazard/-118/34\n" +
-                url + "/hazard/-118/34/BC\n" +
-                url + "/hazard/-118/34/BC/PGA\n" +
-                "```\n")
-        .append("</details>")
-        .toString();
-  }
 }
diff --git a/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java
new file mode 100644
index 0000000000000000000000000000000000000000..64e880c9421b958b413f53fa8e46b2d42a45fb7f
--- /dev/null
+++ b/src/hazard/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/SwaggerHazardCurves.java
@@ -0,0 +1,108 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.io.IOException;
+import java.util.List;
+
+import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
+import gov.usgs.earthquake.nshmp.www.SwaggerUtils;
+
+import io.micronaut.http.HttpRequest;
+import io.swagger.v3.oas.models.OpenAPI;
+
+/**
+ * Swagger page updates for hazard services.
+ *
+ * @author U.S. Geological Survey
+ */
+public class SwaggerHazardCurves extends Swagger<NetcdfServiceHazardCurves> {
+
+  SwaggerHazardCurves(HttpRequest<?> request, NetcdfServiceHazardCurves service) {
+    super(request, service);
+  }
+
+  @Override
+  String description() {
+    StringBuilder builder = new StringBuilder();
+
+    service.netcdfDataFiles()
+        .forEach(netcdf -> {
+          ScienceBaseMetadata metadata = netcdf.netcdfData().scienceBaseMetadata();
+          builder
+              .append(metadata.description + "\n")
+              .append(parameterSection(netcdf.netcdfData()) + "\n")
+              .append(scienceBaseSection(metadata) + "\n");
+        });
+
+    return builder.toString();
+  }
+
+  @Override
+  String descriptionHeader() {
+    return "";
+  }
+
+  @Override
+  String serviceInfo() {
+    return "";
+  }
+
+  @Override
+  String servicePatternSection() {
+    var url = NetcdfWsUtils.getRequestUrl(request);
+    url = url.endsWith("/swagger") ? url.replace("/swagger", "") : url;
+
+    return new StringBuilder()
+        .append(
+            "<details>\n" +
+                "<summary>Service Call Patterns</summary>\n")
+        .append(
+            "### Query Pattern\n" +
+
+                "The query based service call is in the form of:\n" +
+
+                "```text\n" +
+                url + "/hazard?longitude={number}&latitude={number}\n" +
+                url + "/hazard?longitude={number}&latitude={number}&siteClass={string}\n" +
+                url + "/hazard" +
+                "?longitude={number}&latitude={number}&siteClass={string}&imt={string}\n" +
+                "````\n" +
+
+                "Example:\n" +
+                "```text\n" +
+                url + "/hazard?longitude=-118&latitude=34\n" +
+                url + "/hazard?longitude=-118&latitude=34&siteClass=BC\n" +
+                url + "/hazard?longitude=-118&latitude=34&siteClass=BC&imt=PGA\n" +
+                "```\n")
+        .append(
+            "### Slash Pattern\n" +
+
+                "The slash based service call is in the form of:\n" +
+
+                "```text\n" +
+                url + "/hazard/{longitude}/{latitude}\n" +
+                url + "/hazard/{longitude}/{latitude}/{siteClass}\n" +
+                url + "/hazard/{longitude}/{latitude}/{siteClass}/{imt}\n" +
+                "```\n" +
+
+                "Example:\n" +
+
+                "```text\n" +
+                url + "/hazard/-118/34\n" +
+                url + "/hazard/-118/34/BC\n" +
+                url + "/hazard/-118/34/BC/PGA\n" +
+                "```\n")
+        .append("</details>")
+        .toString();
+  }
+
+  @Override
+  OpenAPI updateOpenApi() throws IOException {
+    OpenAPI openApi = super.updateOpenApi();
+
+    SwaggerUtils.imtSchema(
+        openApi.getComponents().getSchemas(),
+        List.copyOf(service.netcdfDataFiles().imts()));
+
+    return openApi;
+  }
+}
diff --git a/src/hazard/src/main/resources/application.yml b/src/hazard/src/main/resources/application.yml
index 006db385cd7a52c1b5aec2c514fb18b6b591dd12..eb7e65e318252797d30050f75cb61f70d7ce8688 100644
--- a/src/hazard/src/main/resources/application.yml
+++ b/src/hazard/src/main/resources/application.yml
@@ -16,4 +16,5 @@ micronaut:
         logger-name: http
 
 nshmp-ws-static:
-  netcdf-file: ${netcdf:src/main/resources/hazard-example.nc}
+  # Path to hazard NetCDF file
+  netcdf-path: ${netcdf:src/main/resources/hazard-example.nc}
diff --git a/src/hazard/src/main/resources/hazard-example.nc b/src/hazard/src/main/resources/hazard-example.nc
index ffdecae92f16cb6c34404bea8e91d41e482712d3..1969150bcb1f553bd4e701ed35ea61c4fd8a96f9 100644
Binary files a/src/hazard/src/main/resources/hazard-example.nc and b/src/hazard/src/main/resources/hazard-example.nc differ
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java
index 5c13df52084f3a2bcac317f429588f895edd4007..b18e4c5a77aa49d14655f0a9ce0a953a002a3538 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/Netcdf.java
@@ -18,10 +18,9 @@ import gov.usgs.earthquake.nshmp.netcdf.reader.Reader;
  *
  * @author U.S. Geological Survey
  */
-public abstract class Netcdf<T> {
+public abstract class Netcdf<T> implements Comparable<Netcdf<T>> {
 
   protected final Path netcdfPath;
-  protected final NetcdfDataType dataType;
   protected final NetcdfData netcdfData;
   protected NetcdfShape netcdfShape;
 
@@ -39,7 +38,12 @@ public abstract class Netcdf<T> {
       throw new IllegalArgumentException("Path to Netcdf file [" + netcdfPath + "] does not exist");
     }
 
-    dataType = NetcdfDataType.getDataType(netcdfPath);
+    Path fileName = netcdfPath.getFileName();
+
+    if (fileName == null || !fileName.toString().endsWith(".nc")) {
+      throw new IllegalArgumentException("NetCDF file not found (.nc) " + netcdfPath);
+    }
+
     var reader = getNetcdfData(netcdfPath);
     netcdfData = reader.netcdfData();
     netcdfShape = reader.netcdfShape();
@@ -52,13 +56,6 @@ public abstract class Netcdf<T> {
    */
   public abstract BoundingData<T> boundingData(Location site);
 
-  /**
-   * Returns the data type.
-   */
-  public NetcdfDataType dataType() {
-    return dataType;
-  }
-
   /**
    * Returns the NetCDF data.
    */
@@ -93,5 +90,11 @@ public abstract class Netcdf<T> {
    */
   public abstract T staticData(Location site, NehrpSiteClass siteClass);
 
+  @Override
+  public int compareTo(Netcdf<T> that) {
+    return this.netcdfData().scienceBaseMetadata().label
+        .compareTo(that.netcdfData().scienceBaseMetadata().label);
+  }
+
   abstract Reader getNetcdfData(Path netcdfPath);
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFiles.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFiles.java
new file mode 100644
index 0000000000000000000000000000000000000000..18c1690d657d41f7c2695e28029a19aa3095bae0
--- /dev/null
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataFiles.java
@@ -0,0 +1,76 @@
+package gov.usgs.earthquake.nshmp.netcdf;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
+
+/**
+ * Read in all NetCDF files associated with a {@link NetcdfDataType}, holds
+ * {@link Netcdf} objects associated with each NetCDF file.
+ *
+ * @author U.S. Geological Survey
+ */
+public abstract class NetcdfDataFiles<T extends Netcdf<?>> extends ArrayList<T> {
+  private final Path netcdfPath;
+  private final NetcdfDataType dataType;
+
+  public NetcdfDataFiles(Path netcdfPath, NetcdfDataType dataType) {
+    super();
+    this.netcdfPath = netcdfPath;
+    this.dataType = dataType;
+  }
+
+  /**
+   * Returns the path to the directory where all NetCDF files are.
+   */
+  public Path netcdfPath() {
+    return netcdfPath;
+  }
+
+  /**
+   * Returns the data type of the NetCDF files.
+   */
+  public NetcdfDataType dataType() {
+    return dataType;
+  }
+
+  /**
+   * Returns the set of NEHRP site classes for all data files.
+   */
+  public Set<NehrpSiteClass> siteClasses() {
+    return stream()
+        .flatMap(netcdf -> netcdf.netcdfData().siteClasses().stream())
+        .sorted()
+        .collect(Collectors.toSet());
+  }
+
+  /**
+   * Find NetCDF files (.nc) with specific {@link NetcdfDataType}.
+   *
+   * @param netcdfPath Path to NetCDF files
+   * @param dataType The data type to filter by
+   * @throws IOException
+   */
+  protected Stream<Path> walkFiles(Path netcdfPath, NetcdfDataType dataType) throws IOException {
+    return Files.walk(netcdfPath)
+        .filter(file -> file.getFileName().toString().endsWith(".nc"))
+        .filter(file -> NetcdfDataType.getDataType(file) == dataType);
+  }
+
+  /**
+   * Returns the list of {@link Netcdf} objects associated with a NetCDF file
+   * and {@link NetcdfDataType}.
+   *
+   * @param netcdfPath The path to NetCDF files
+   * @param dataType The data type to read in
+   * @throws IOException
+   */
+  protected abstract List<T> readFiles(Path netcdfPath, NetcdfDataType dataType);
+}
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataType.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataType.java
index d7bb13c55cb393d6fad60b864e903487580fee39..11196fac65c5b1306db819dbdff68bac8c1bb7cf 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataType.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/NetcdfDataType.java
@@ -1,12 +1,10 @@
 package gov.usgs.earthquake.nshmp.netcdf;
 
-import java.io.IOException;
 import java.nio.file.Path;
 
+import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils;
 import gov.usgs.earthquake.nshmp.netcdf.reader.NetcdfUtils.Key;
 
-import ucar.nc2.dataset.NetcdfDatasets;
-
 /**
  * Supported NetCDF data types.
  */
@@ -21,12 +19,7 @@ public enum NetcdfDataType {
    * @param netcdfPath Path to NetCDF file
    */
   public static NetcdfDataType getDataType(Path netcdfPath) {
-    try (var ncd = NetcdfDatasets.openDataset(netcdfPath.toString())) {
-      var group = ncd.getRootGroup();
-      var vDataType = group.attributes().findAttribute(Key.DATA_TYPE);
-      return NetcdfDataType.valueOf(vDataType.getStringValue());
-    } catch (IOException e) {
-      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + " ]");
-    }
+    return NetcdfDataType
+        .valueOf(NetcdfUtils.readAttribute(Key.DATA_TYPE, netcdfPath).getStringValue());
   }
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfData.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfData.java
index 7037e9c14ac8244250af619ee528dea81b55aa8c..f113edc7ed945a72b03077e27fd789063d181a7c 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfData.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/data/NetcdfData.java
@@ -118,6 +118,16 @@ public class NetcdfData {
 
     Builder() {}
 
+    public static Builder copyOf(NetcdfData netcdfData) {
+      return builder()
+          .imts(netcdfData.imts)
+          .latitudes(netcdfData.latitudes)
+          .longitudes(netcdfData.longitudes)
+          .scienceBaseMetadata(netcdfData.scienceBaseMetadata)
+          .siteClasses(netcdfData.siteClasses)
+          .vs30Map(netcdfData.vs30Map);
+    }
+
     public Builder imts(List<Imt> imts) {
       this.imts = imts;
       return this;
@@ -162,5 +172,4 @@ public class NetcdfData {
       checkState(!vs30Map.isEmpty(), "Must add vs30s");
     }
   }
-
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
index 186b1465c5508df884a450b96ae955b084fa50e4..8ccccd717b07e78844f146cb2b44fc2188819ba4 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/reader/NetcdfUtils.java
@@ -4,6 +4,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Arrays;
 
 import com.google.common.math.DoubleMath;
@@ -13,7 +14,10 @@ import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.LocationList;
 
 import ucar.ma2.DataType;
+import ucar.nc2.Attribute;
 import ucar.nc2.Group;
+import ucar.nc2.dataset.NetcdfDataset;
+import ucar.nc2.dataset.NetcdfDatasets;
 
 public class NetcdfUtils {
 
@@ -190,6 +194,15 @@ public class NetcdfUtils {
     return (String[]) get1DArray(group, key, DataType.STRING);
   }
 
+  public static Attribute readAttribute(String attributeKey, Path netcdfPath) {
+    try (NetcdfDataset ncd = NetcdfDatasets.openDataset(netcdfPath.toString())) {
+      Group group = ncd.getRootGroup();
+      return group.attributes().findAttribute(attributeKey);
+    } catch (IOException e) {
+      throw new RuntimeException("Could not read Netcdf file [" + netcdfPath + " ]");
+    }
+  }
+
   public static class Key {
     public static final String DESCRIPTION = "description";
     public static final String GRID_STEP = "gridStep";
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java
index c228a0b1223ea92d2b75d1ed60b817ab0d4e7e53..a3485d039a5eff1015376e8b1f67c63f9d98ae58 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Metadata.java
@@ -1,12 +1,7 @@
 package gov.usgs.earthquake.nshmp.netcdf.www;
 
-import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
-import gov.usgs.earthquake.nshmp.netcdf.Netcdf;
-import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
-import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfService.SourceModel;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData;
-import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataImt;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
 import gov.usgs.earthquake.nshmp.netcdf.www.meta.DoubleParameter;
 
@@ -17,17 +12,13 @@ import io.micronaut.http.HttpRequest;
  *
  * @author U.S. Geological Survey
  */
-public class Metadata<T extends Query> {
+public class Metadata<S, T extends Query> {
   public final String description;
   public final String[] syntax;
-  public final SourceModel model;
-  public final DoubleParameter longitude;
-  public final DoubleParameter latitude;
+  public final S models;
   public final DoubleParameter vs30;
-  public final NetcdfMetadata netcdfMetadata;
 
-  Metadata(HttpRequest<?> request, NetcdfService<T> netcdfService, String description) {
-    var netcdf = netcdfService.netcdf();
+  Metadata(HttpRequest<?> request, NetcdfService<S, T> netcdfService, String description) {
     var url = request.getUri().toString();
     url = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
     this.description = description;
@@ -38,37 +29,13 @@ public class Metadata<T extends Query> {
         url + "?longitude={number}&latitude={number}&siteClass={NehrpSiteClass}",
     };
 
-    var min = netcdf.netcdfData().minimumBounds();
-    var max = netcdf.netcdfData().maximumBounds();
+    models = netcdfService.getSourceModels();
 
-    longitude = new DoubleParameter(
-        "Longitude",
-        "°",
-        min.longitude,
-        max.longitude);
-    latitude = new DoubleParameter(
-        "Latitude",
-        "°",
-        min.latitude,
-        max.latitude);
-    model = netcdfService.getSourceModel();
     vs30 = new DoubleParameter(
         "Vs30",
         "m/s",
         150,
         1500);
-    netcdfMetadata = new NetcdfMetadata(netcdf);
-  }
-
-  class NetcdfMetadata {
-    public final String netcdfFile;
-    public final ScienceBaseMetadata scienceBaseMetadata;
-
-    NetcdfMetadata(Netcdf<?> netcdf) {
-      var fileName = netcdf.netcdfPath().getFileName();
-      netcdfFile = fileName == null ? netcdf.netcdfPath().toString() : fileName.toString();
-      scienceBaseMetadata = netcdf.netcdfData().scienceBaseMetadata();
-    }
   }
 
   interface ServiceMetadata {
@@ -94,18 +61,4 @@ public class Metadata<T extends Query> {
       return new RequestDataSiteClass(requestData.site, siteClass, requestData.format);
     }
   }
-
-  static class HazardResponseMetadata extends ServiceResponseMetadata {
-    public final Imt imt;
-
-    HazardResponseMetadata(NehrpSiteClass siteClass, Imt imt, String xLabel, String yLabel) {
-      super(siteClass, xLabel, yLabel);
-      this.imt = imt;
-    }
-
-    @Override
-    public RequestDataImt toRequestMetadata(RequestData requestData) {
-      return new RequestDataImt(requestData.site, siteClass, imt, requestData.format);
-    }
-  }
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java
index 243ab9418da29a3112e98a87576af188613d6c18..9d88e3cfa083f37dae1b03ff0ffc6c271e2a248e 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfService.java
@@ -12,10 +12,12 @@ import gov.usgs.earthquake.nshmp.Text;
 import gov.usgs.earthquake.nshmp.Text.Delimiter;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
 import gov.usgs.earthquake.nshmp.netcdf.Netcdf;
+import gov.usgs.earthquake.nshmp.netcdf.NetcdfDataFiles;
+import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
 import gov.usgs.earthquake.nshmp.netcdf.www.Metadata.ServiceResponseMetadata;
 import gov.usgs.earthquake.nshmp.netcdf.www.Query.Service;
 import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestData;
-import gov.usgs.earthquake.nshmp.netcdf.www.Request.RequestDataSiteClass;
+import gov.usgs.earthquake.nshmp.netcdf.www.meta.DoubleParameter;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
 
 import io.micronaut.http.HttpRequest;
@@ -28,14 +30,14 @@ import io.micronaut.http.HttpResponse;
  *
  * @author U.S. Geological Survey
  */
-public abstract class NetcdfService<T extends Query> {
+public abstract class NetcdfService<S, T extends Query> {
 
   protected static final Logger LOGGER = Logger.getLogger(NetcdfService.class.getName());
 
-  Netcdf<?> netcdf;
+  protected NetcdfDataFiles<?> netcdfDataFiles;
 
-  protected NetcdfService(Netcdf<?> netcdf) {
-    this.netcdf = netcdf;
+  protected NetcdfService(NetcdfDataFiles<?> netcdfDataFiles) {
+    this.netcdfDataFiles = netcdfDataFiles;
   }
 
   /**
@@ -43,42 +45,17 @@ public abstract class NetcdfService<T extends Query> {
    *
    * @param httpRequest The HTTP request
    */
-  abstract ResponseBody<String, Metadata<T>> getMetadataResponse(HttpRequest<?> httpRequest);
+  abstract ResponseBody<String, Metadata<S, T>> getMetadataResponse(HttpRequest<?> httpRequest);
 
   /**
    * Returns the service name
    */
   abstract String getServiceName();
 
-  /**
-   * Returns the source model
-   */
-  abstract SourceModel getSourceModel();
-
   /**
    * Returns the netcdf object associated with the specific data type.
    */
-  abstract Netcdf<?> netcdf();
-
-  /**
-   * Returns the static curves at a specific location.
-   *
-   * @param <T> The response type
-   * @param request The request data
-   * @param url The URL for the service call
-   */
-  abstract <U> ResponseBody<RequestData, U> processCurves(RequestData request, String url);
-
-  /**
-   * Returns the static curves at a specific location and site class.
-   *
-   * @param <T> The response type
-   * @param request The request data
-   * @param url The URL for the service call
-   */
-  abstract <U> ResponseBody<RequestDataSiteClass, U> processCurvesSiteClass(
-      RequestDataSiteClass request,
-      String url);
+  abstract NetcdfDataFiles<?> netcdfDataFiles();
 
   /**
    * Process the service request and returns the string response.
@@ -127,6 +104,11 @@ public abstract class NetcdfService<T extends Query> {
     }
   }
 
+  /**
+   * Returns the source models.
+   */
+  abstract S getSourceModels();
+
   <U extends ServiceResponseMetadata> String toCsvResponse(
       RequestData requestData,
       ResponseData<U> responseData) {
@@ -192,13 +174,41 @@ public abstract class NetcdfService<T extends Query> {
     return lines.stream().collect(Collectors.joining(Text.NEWLINE));
   }
 
+  static class NetcdfMetadata {
+    public final String netcdfFile;
+    public final ScienceBaseMetadata scienceBaseMetadata;
+
+    NetcdfMetadata(Netcdf<?> netcdf) {
+      var fileName = netcdf.netcdfPath().getFileName();
+      netcdfFile = fileName == null ? netcdf.netcdfPath().toString() : fileName.toString();
+      scienceBaseMetadata = netcdf.netcdfData().scienceBaseMetadata();
+    }
+  }
+
   static class SourceModel {
     public final String name;
     public final Map<NehrpSiteClass, Double> siteClasses;
+    public final DoubleParameter longitude;
+    public final DoubleParameter latitude;
+    public final NetcdfMetadata netcdfMetadata;
 
     SourceModel(Netcdf<?> netcdf) {
       name = netcdf.netcdfData().scienceBaseMetadata().label;
       siteClasses = netcdf.netcdfData().vs30Map();
+      var min = netcdf.netcdfData().minimumBounds();
+      var max = netcdf.netcdfData().maximumBounds();
+
+      longitude = new DoubleParameter(
+          "Longitude",
+          "°",
+          min.longitude,
+          max.longitude);
+      latitude = new DoubleParameter(
+          "Latitude",
+          "°",
+          min.latitude,
+          max.latitude);
+      netcdfMetadata = new NetcdfMetadata(netcdf);
     }
   }
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java
index c4233ad557e292cf3b4a1fd20eb1a9790c8ac8fb..cc9a932302868e95c509b888fa8855412a56e54c 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/NetcdfWsUtils.java
@@ -3,29 +3,21 @@ package gov.usgs.earthquake.nshmp.netcdf.www;
 import static com.google.common.base.CaseFormat.UPPER_CAMEL;
 import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
 
-import java.io.IOException;
-import java.util.Arrays;
 import java.util.Optional;
 import java.util.logging.Logger;
-import java.util.stream.Collectors;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
 import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.netcdf.NetcdfVersion;
-import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
-import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
 import gov.usgs.earthquake.nshmp.www.ResponseBody;
 import gov.usgs.earthquake.nshmp.www.ResponseMetadata;
-import gov.usgs.earthquake.nshmp.www.SwaggerUtils;
 import gov.usgs.earthquake.nshmp.www.WsUtils.EnumSerializer;
 import gov.usgs.earthquake.nshmp.www.WsUtils.NaNSerializer;
 
 import io.micronaut.http.HttpRequest;
 import io.micronaut.http.HttpResponse;
-import io.swagger.v3.oas.models.OpenAPI;
-import io.swagger.v3.parser.OpenAPIV3Parser;
 
 public class NetcdfWsUtils {
   static final Gson GSON;
@@ -69,33 +61,6 @@ public class NetcdfWsUtils {
     return HttpResponse.serverError(response);
   }
 
-  public static OpenAPI getOpenApi(
-      HttpRequest<?> request,
-      NetcdfData netcdfData,
-      String serviceSection) throws IOException {
-    var openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-ws-static.yml");
-    var scienceBaseMetadata = netcdfData.scienceBaseMetadata();
-    SwaggerUtils.addLocationBounds(openApi, netcdfData.minimumBounds(), netcdfData.maximumBounds());
-    var components = openApi.getComponents();
-    var schemas = components.getSchemas();
-    SwaggerUtils.siteClassSchema(schemas, netcdfData.siteClasses());
-    openApi.servers(null);
-
-    openApi.getInfo().setTitle(scienceBaseMetadata.label);
-
-    // Update description
-    var description = new StringBuilder()
-        .append(scienceBaseMetadata.description + "\n")
-        .append(swaggerResponseFormatSection())
-        .append(serviceSection)
-        .append(swaggerParameterSection(netcdfData))
-        .append(swaggerScienceBaseSection(scienceBaseMetadata))
-        .toString();
-    openApi.getInfo().setDescription(description);
-
-    return openApi;
-  }
-
   public static enum Key {
     LATITUDE,
     LONGITUDE,
@@ -107,52 +72,4 @@ public class NetcdfWsUtils {
       return UPPER_UNDERSCORE.to(UPPER_CAMEL, name());
     }
   }
-
-  private static String swaggerParameterSection(NetcdfData netcdfData) {
-    return new StringBuilder()
-        .append(
-            "<details>\n" +
-                "<summary>Parameters</summary>\n")
-        .append(
-            SwaggerUtils.locationBoundsInfo(netcdfData.minimumBounds(), netcdfData.maximumBounds(),
-                Optional.of("###")))
-        .append(SwaggerUtils.siteClassInfo(netcdfData.siteClasses(), Optional.of("###")))
-        .append("</details>")
-        .toString();
-  }
-
-  private static String swaggerResponseFormatSection() {
-    return new StringBuilder()
-        .append(
-            "<details>\n" +
-                "<summary>Response Format: CSV or JSON</summary>\n")
-        .append(
-            "The web service can respond in JSON or CSV format.\n" +
-                "<br><br>" +
-                "Choose the format type by adding a `format` query to the service call:" +
-                "`?format=JSON` or `?format=CSV`\n\n" +
-                "<br><br>" +
-                "Default: `JSON`")
-        .append("</details>")
-        .toString();
-  }
-
-  private static String swaggerScienceBaseSection(ScienceBaseMetadata scienceBaseMetadata) {
-    return new StringBuilder()
-        .append(
-            "<details>\n" +
-                "<summary>ScienceBase Information</summary>\n")
-        .append("Data history: " + scienceBaseMetadata.history)
-        .append("<br><br>")
-        .append(
-            String.format(
-                "Returned data are spatially interpolated from " +
-                    "the %s grid data from ScienceBase:\n",
-                scienceBaseMetadata.gridStep))
-        .append(Arrays.stream(scienceBaseMetadata.scienceBaseInfo)
-            .map(info -> String.format("- [%s](%s)", info.url, info.url))
-            .collect(Collectors.joining("\n")))
-        .append("</details>")
-        .toString();
-  }
 }
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java
index eb193cbb2c40d17be105336c9a1f8610b185b5cc..af4452de0471c9bee952929e6ae75df963df0556 100644
--- a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Request.java
@@ -5,7 +5,6 @@ import java.util.List;
 import gov.usgs.earthquake.nshmp.Text;
 import gov.usgs.earthquake.nshmp.Text.Delimiter;
 import gov.usgs.earthquake.nshmp.geo.Location;
-import gov.usgs.earthquake.nshmp.gmm.Imt;
 import gov.usgs.earthquake.nshmp.gmm.NehrpSiteClass;
 import gov.usgs.earthquake.nshmp.netcdf.www.NetcdfWsUtils.Key;
 
@@ -46,24 +45,6 @@ public class Request {
     }
   }
 
-  /**
-   * Request data with site class and imt
-   */
-  static class RequestDataImt extends RequestDataSiteClass {
-    public Imt imt;
-
-    RequestDataImt(Location site, NehrpSiteClass siteClass, Imt imt, ResponseFormat format) {
-      super(site, siteClass, format);
-      this.imt = imt;
-    }
-
-    @Override
-    public String toCsv() {
-      return String.format("%s,%s",
-          super.toCsv(), Text.join(List.of(Key.IMT.toString(), imt.name()), Delimiter.COMMA));
-    }
-  }
-
   /**
    * Request data with site class
    */
diff --git a/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Swagger.java b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Swagger.java
new file mode 100644
index 0000000000000000000000000000000000000000..400e5142f06caf3d32f5e622fd09744e6cf9c6a9
--- /dev/null
+++ b/src/lib/src/main/java/gov/usgs/earthquake/nshmp/netcdf/www/Swagger.java
@@ -0,0 +1,157 @@
+package gov.usgs.earthquake.nshmp.netcdf.www;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import gov.usgs.earthquake.nshmp.geo.Bounds;
+import gov.usgs.earthquake.nshmp.geo.Location;
+import gov.usgs.earthquake.nshmp.geo.LocationList;
+import gov.usgs.earthquake.nshmp.netcdf.data.NetcdfData;
+import gov.usgs.earthquake.nshmp.netcdf.data.ScienceBaseMetadata;
+import gov.usgs.earthquake.nshmp.www.SwaggerUtils;
+
+import io.micronaut.http.HttpRequest;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.parser.OpenAPIV3Parser;
+
+/**
+ * Update Swagger landing page.
+ *
+ * @author U.S. Geological Survey
+ */
+abstract class Swagger<T extends NetcdfService<?, ?>> {
+
+  final HttpRequest<?> request;
+  final T service;
+
+  Swagger(HttpRequest<?> request, T service) {
+    this.request = request;
+    this.service = service;
+  }
+
+  /**
+   * Returns the main description header text.
+   */
+  abstract String descriptionHeader();
+
+  /**
+   * Returns the service info text.
+   */
+  abstract String serviceInfo();
+
+  /**
+   * Returns the service patterns sections
+   */
+  abstract String servicePatternSection();
+
+  /**
+   * Creates the main swagger description section.
+   *
+   * @param netcdfDataFiles The data files
+   */
+  abstract String description();
+
+  /**
+   * Creates the parameter section for the description.
+   *
+   * @param netcdfData The NetCDF data
+   */
+  String parameterSection(NetcdfData netcdfData) {
+    return new StringBuilder()
+        .append(
+            "<details>\n" +
+                "<summary>Parameters</summary>\n")
+        .append(
+            SwaggerUtils.locationBoundsInfo(netcdfData.minimumBounds(), netcdfData.maximumBounds(),
+                Optional.of("###")))
+        .append(SwaggerUtils.siteClassInfo(netcdfData.siteClasses(), Optional.of("###")))
+        .append("</details>")
+        .toString();
+  }
+
+  /**
+   * Creates the response format section for the description.
+   */
+  String responseFormatSection() {
+    return new StringBuilder()
+        .append(
+            "<details>\n" +
+                "<summary>Response Format: CSV or JSON</summary>\n")
+        .append(
+            "The web service can respond in JSON or CSV format.\n" +
+                "<br><br>" +
+                "Choose the format type by adding a `format` query to the service call:" +
+                "`?format=JSON` or `?format=CSV`\n\n" +
+                "<br><br>" +
+                "Default: `JSON`")
+        .append("</details>")
+        .toString();
+  }
+
+  /**
+   * Creates the science base section for the description.
+   *
+   * @param scienceBaseMetadata The metadata.
+   */
+  String scienceBaseSection(ScienceBaseMetadata scienceBaseMetadata) {
+    return new StringBuilder()
+        .append(
+            "<details>\n" +
+                "<summary>ScienceBase Information</summary>\n")
+        .append("Data history: " + scienceBaseMetadata.history)
+        .append("<br><br>")
+        .append(
+            String.format(
+                "Returned data are spatially interpolated from " +
+                    "the %s grid data from ScienceBase:\n",
+                scienceBaseMetadata.gridStep))
+        .append(Arrays.stream(scienceBaseMetadata.scienceBaseInfo)
+            .map(info -> String.format("- [%s](%s)", info.url, info.url))
+            .collect(Collectors.joining("\n")))
+        .append("</details>")
+        .toString();
+  }
+
+  void updateLocationBounds(OpenAPI openApi) {
+    List<Location> locations = service.netcdfDataFiles().stream()
+        .map(netcdf -> netcdf.netcdfData())
+        .flatMap(
+            netcdfData -> List.of(netcdfData.minimumBounds(), netcdfData.maximumBounds()).stream())
+        .collect(Collectors.toList());
+
+    Bounds bounds = LocationList.copyOf(locations).bounds();
+    SwaggerUtils.addLocationBounds(openApi, bounds.min, bounds.max);
+  }
+
+  /**
+   * Update the {@link OpenAPI} documentation.
+   *
+   * @throws IOException
+   */
+  OpenAPI updateOpenApi() throws IOException {
+    OpenAPI openApi = new OpenAPIV3Parser().read("META-INF/swagger/nshmp-ws-static.yml");
+    updateLocationBounds(openApi);
+    Components components = openApi.getComponents();
+    Map<String, Schema> schemas = components.getSchemas();
+    SwaggerUtils.siteClassSchema(schemas, List.copyOf(service.netcdfDataFiles().siteClasses()));
+    openApi.servers(null);
+
+    openApi.getInfo().setTitle(service.getServiceName());
+
+    // Update description
+    String description = new StringBuilder()
+        .append(description())
+        .append("## Formating \n" + responseFormatSection() + "\n")
+        .append("## Service Patterns \n" + servicePatternSection())
+        .toString();
+    openApi.getInfo().setDescription(description);
+
+    return openApi;
+  }
+}