npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@deepinnet/service-mvt-middleware

v0.0.2

Published

NestJS middleware module for serving 2D MVT and custom 3D vector tile endpoints.

Readme

@deepinnet/service-mvt-middleware

NestJS 中间件包,用于把二维 MVT 和三维自定义 PBF 瓦片服务接到 HTTP 控制器层。

这个包只负责 NestJS 集成层:

  • 暴露二维 /tiles/... 路由
  • 暴露三维 /tiles-3d/... 路由
  • 暴露 /tiles/:layer/metadata/health
  • 把底层 @deepinnet/service-mvt-middleware-core 引擎接到 Nest 应用

如果你不需要 NestJS,只想直接使用瓦片引擎和 PostgreSQL adapter,请改用:

  • @deepinnet/service-mvt-middleware-core

安装

pnpm add @deepinnet/service-mvt-middleware
pnpm add @deepinnet/service-mvt-middleware-core

适用场景

  • 你在 NestJS 中提供二维矢量瓦片接口
  • 你在 NestJS 中提供三维自定义 PBF 接口
  • 你希望把图层配置、控制器和健康检查统一放在一个 module 中

怎么使用

最小接入步骤通常是:

  1. 在数据库中准备好 PostGIS 图层表或视图。
  2. 在 Nest AppModule 中注册 TileModule.forRoot(...)
  3. 配置 layersdatabasedefaults
  4. 启动应用后,通过 /tiles/.../tiles-3d/... 请求瓦片。

1. 在 Nest 中注册模块

import { Module } from "@nestjs/common";
import { TileModule } from "@deepinnet/service-mvt-middleware";

@Module({
  imports: [
    TileModule.forRoot({
      layers: [
        {
          layer: "data",
          sourceType: "table",
          sourceObject: "public.geom",
          idField: "id",
          geomField: "geometry",
          geom3dField: "geometry3d",
          sourceCrs: "4326",
          defaultOutputCrs: "gcj02",
          supportedOutputCrs: ["4326", "gcj02"],
          propertyWhitelist: ["name", "color"],
          gzip: {
            enabled: true,
          },
          clusterEnabled: true,
          clusterMaxZoom: 15,
          clusterCellSize: 256,
          clusterMaxFeatures: 2048,
          clusterFields: {
            color: {
              field: "color",
              op: "top",
            },
          },
        },
      ],
      database: {
        connectionString: process.env.DATABASE_URL,
      },
      defaults: {
        sourceCrs: "4326",
        defaultOutputCrs: "gcj02",
      },
      tile: {
        extent: 4096,
        buffer: 64,
        maxFeatures: 20000,
      },
      controllers: {
        tile: true,
        tile3d: true,
        metadata: true,
        health: true,
      },
    }),
  ],
})
export class AppModule {}

1.1 复用已有 pg.Pool

如果你的应用已经自己管理 PostgreSQL 连接池,可以直接把现有 pool 传给 database.pool

import { Module } from "@nestjs/common";
import { Pool } from "pg";
import { TileModule } from "@deepinnet/service-mvt-middleware";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

@Module({
  imports: [
    TileModule.forRoot({
      layers: [
        {
          layer: "data",
          sourceType: "table",
          sourceObject: "public.geom",
          idField: "id",
          geomField: "geometry",
        },
      ],
      database: {
        pool,
      },
    }),
  ],
})
export class AppModule {}

这种方式下,瓦片查询和默认 health check 都会复用你传入的 pool

连接池生命周期需要区分两种情况:

  • database.connectionStringdatabase.poolConfig:模块内部会创建连接池,并在应用关闭时自动 end()
  • database.pool:连接池归调用方所有,这个包不会自动关闭,你需要在自己的关闭流程里手动执行 await pool.end()

1.2 配置统一路由前缀

如果你希望把默认路由整体挂到统一前缀下,可以配置 routePrefix

TileModule.forRoot({
  routePrefix: "api/v1",
  layers: [
    {
      layer: "data",
      sourceType: "table",
      sourceObject: "public.geom",
      idField: "id",
      geomField: "geometry",
    },
  ],
})

这样默认路由会变成:

  • /api/v1/tiles/:layer/:z/:x/:y.pbf
  • /api/v1/tiles-3d/:layer/:z/:x/:y.pbf
  • /api/v1/tiles/:layer/metadata
  • /api/v1/health

routePrefix 会自动去掉首尾多余的 /,所以 "api""/api""/api/" 的效果一致。

2. 启动后默认会暴露哪些接口

启用默认 controllers 后,会暴露这些路由:

  • GET /tiles/:layer/:z/:x/:y.pbf
  • GET /tiles-3d/:layer/:z/:x/:y.pbf
  • GET /tiles/:layer/metadata
  • GET /health

如果配置了 routePrefix: "api",上面四类路由会统一变成 /api/...

3. 怎么发请求

二维示例:

curl --compressed -i \
  "http://127.0.0.1:3000/tiles/data/8/209/111.pbf?crs=gcj02&fields=name,color"

三维示例:

curl --compressed -i \
  "http://127.0.0.1:3000/tiles-3d/data/8/209/111.pbf?crs=gcj02"

读取 metadata:

curl "http://127.0.0.1:3000/tiles/data/metadata"

读取 health:

curl "http://127.0.0.1:3000/health"

HTTP 接口详解

二维接口

GET /tiles/:layer/:z/:x/:y.pbf

返回内容:

  • 标准二维 MVT
  • Content-Type: application/vnd.mapbox-vector-tile
  • 如果开启 gzip.enabled=true,响应头会带 Content-Encoding: gzip

适合的前端消费方式:

  • MapLibre / Mapbox GL 风格 vector source
  • 其他能直接消费 MVT 的前端引擎

三维接口

GET /tiles-3d/:layer/:z/:x/:y.pbf

返回内容:

  • 自定义三维 PBF
  • Content-Type: application/octet-stream
  • 如果开启 gzip.enabled=true,响应头会带 Content-Encoding: gzip

适合的前端消费方式:

  • 自己在前端拿到 ArrayBuffer 后解码
  • 不能直接当标准 MVT 使用

metadata 接口

GET /tiles/:layer/metadata

返回当前图层的配置摘要,常用于:

  • 前端初始化前先确认图层信息
  • 联调时检查图层名、坐标系、字段白名单
  • 运维或健康排查

health 接口

GET /health

返回服务和数据库探针状态,常用于:

  • 容器健康检查
  • 上线后巡检
  • 联调时确认数据库是否连通

参数怎么传

路径参数

二维和三维接口都使用同一组路径参数:

| 参数 | 类型 | 是否必填 | 含义 | 示例 | | --- | --- | --- | --- | --- | | layer | string | 是 | 图层名,必须命中 layers 配置中的某一项。 | data | | z | number | 是 | 缩放级别,必须是大于等于 0 的整数。 | 8 | | x | number | 是 | 当前 z 层级下的瓦片列号。 | 209 | | y | number | 是 | 当前 z 层级下的瓦片行号。 | 111 |

查询参数

二维和三维接口共用同一组查询参数:

| 参数 | 类型 | 是否必填 | 含义 | 示例 | | --- | --- | --- | --- | --- | | crs | string | 否 | 输出坐标系,当前支持 4326gcj02。未传时使用图层的 defaultOutputCrs。 | crs=gcj02 | | minZoom | number | 否 | 请求级最小缩放限制,会与图层和默认配置一起合并。 | minZoom=6 | | maxZoom | number | 否 | 请求级最大缩放限制,会与图层和默认配置一起合并。 | maxZoom=16 | | fields | string | 否 | 逗号分隔的属性字段列表。最终仍受 propertyWhitelist 约束。 | fields=name,color | | from | string | 否 | 起始时间,要求图层配置了 timeField。 | from=2026-01-01T00:00:00Z | | to | string | 否 | 结束时间,要求图层配置了 timeField。 | to=2026-12-31T23:59:59Z | | v | string | 否 | 版本字符串,主要参与缓存键区分。 | v=2026-04-30 |

/tiles/:layer/metadata 只使用路径参数 layer/health 不接收业务参数。

如果你需要过滤条件,请在服务端内部构造 TileQueryInput.filter 后再调用引擎。默认 HTTP controller 不会从 URL 读取 filter;历史上的 ?filter=... 也会被静默忽略。

filter 现在怎么传

默认公开 HTTP 接口已经移除了 filter 查询参数:

  • 不再支持 GET /tiles/...?...&filter=...
  • 不再支持 GET /tiles-3d/...?...&filter=...
  • 旧的 ?filter=... 不会报错,但会被静默忽略

这样做的目的是不再把过滤条件暴露在 URL、访问日志、缓存键观测面和浏览器历史里。

如果你仍然需要过滤能力,请在服务器内部直接传 TileQueryInput.filter,而不是让客户端拼到链接上。

Nest 服务内部传 filter

最直接的方式,是在你自己的 Controller / Service 里调用 TileService.getTile(...)TileService.get3DTile(...),把 filter 放进 TileQueryInput

import { Controller, Get, Inject, Param, Res } from "@nestjs/common";
import type { TileFilterExpression } from "@deepinnet/service-mvt-middleware-core";
import { TileHttpResponder, TileService } from "@deepinnet/service-mvt-middleware";

const onlyPoint1: TileFilterExpression = {
  op: "eq",
  field: "name",
  value: "point-1",
};

@Controller("internal-tiles")
export class InternalTileController {
  constructor(
    @Inject(TileService) private readonly tileService: TileService,
    @Inject(TileHttpResponder) private readonly tileHttpResponder: TileHttpResponder,
  ) {}

  @Get(":layer/:z/:x/:y.pbf")
  async getTile(
    @Param("layer") layer: string,
    @Param("z") z: string,
    @Param("x") x: string,
    @Param("y") y: string,
    @Res() response?: unknown,
  ): Promise<void> {
    const result = await this.tileService.getTile({
      layer,
      z: Number(z),
      x: Number(x),
      y: Number(y),
      crs: "4326",
      fields: ["name", "color"],
      filter: onlyPoint1,
    });

    this.tileHttpResponder.write(response, result);
  }
}

filter 能传什么

filterTileQueryInput 的一个内部字段,类型是 TileFilterExpression。当前支持的结构化操作符仍然是:

  • eq
  • in
  • range
  • and
  • or

它仍然会经过底层白名单校验和参数化 SQL 生成;也就是说,filter 只是从公开 URL 上移除了,并没有从服务端内部能力里删除。

返回结果怎么理解

二维

  • 成功时通常返回 200
  • 无命中或超出缩放范围时通常返回 204
  • body 是标准 MVT 二进制
  • 若开启 gzip,客户端需要支持 gzip 解压

三维

  • 成功时通常返回 200
  • 无命中或超出缩放范围时通常返回 204
  • body 是自定义 3D PBF 二进制
  • 若开启 gzip,客户端需要支持 gzip 解压

常见错误

返回结构一般是:

{
  "statusCode": 400,
  "code": "INVALID_TILE_QUERY",
  "message": "具体错误信息",
  "details": {}
}

常见错误原因:

  • z/x/y 不是合法瓦片坐标
  • layer 不存在
  • crs 不在图层允许范围内
  • from/to 未配置 timeField
  • 数据库连接失败

TileModule 配置怎么写

TileModule.forRoot(...)

最常用配置项如下:

| 参数 | 类型 | 含义 | | --- | --- | --- | | layers | LayerConfigInput[] \| Record<string, LayerConfigInput> | 图层配置集合,定义可访问的图层、几何字段、输出坐标系、cluster、gzip 等能力。 | | database | PostgresDatabaseOptions | PostgreSQL / PostGIS 连接配置,会用于默认的二维和三维查询执行器。 | | controllers | false \| TileControllerOptions | 控制器开关,可按需启用二维、三维、metadata 和 health 路由。 | | routePrefix | string | 给默认 controller 增加统一前缀,例如 "api" 会生成 /api/tiles/api/health。 | | defaults | TileEngineDefaults | 模块级默认配置,会在图层未显式声明时作为回退值。 | | tile | TileRenderOptions | 瓦片渲染参数,例如 extentbuffermaxFeatures。 | | healthIndicator | () => Promise<Record<string, unknown>> | 自定义健康检查函数,用于覆盖默认数据库探针。 |

controllers 子项

| 参数 | 类型 | 含义 | | --- | --- | --- | | tile | boolean | 是否启用二维接口 /tiles/...。 | | tile3d | boolean | 是否启用三维接口 /tiles-3d/...。 | | metadata | boolean | 是否启用 /tiles/:layer/metadata。 | | health | boolean | 是否启用 /health。 |

defaults 常用子项

| 参数 | 类型 | 含义 | | --- | --- | --- | | sourceCrs | string | 图层未显式声明时的默认源坐标系。 | | defaultOutputCrs | string | 图层未显式声明时的默认输出坐标系。 | | minZoom | number | 图层未显式声明时的默认最小缩放限制。 | | maxZoom | number | 图层未显式声明时的默认最大缩放限制。 | | clusterEnabled | boolean | 图层未显式声明时是否默认开启 cluster。 | | clusterMaxZoom | number | 图层未显式声明时,cluster 的切换阈值。 | | clusterCellSize | number | 图层未显式声明时,cluster 网格尺寸。 | | clusterMaxFeatures | number | 图层未显式声明时,cluster 输出要素上限。 | | clusterFields | object | 图层未显式声明时,聚合属性字段配置。 | | clusterAltitudeMode | string | 三维 cluster 的高程聚合规则,当前支持 max。 | | gzip | object | 响应压缩配置,gzip.enabled=true 时二维和三维接口都会返回 gzip 响应。 |

database 常用子项

| 参数 | 类型 | 含义 | | --- | --- | --- | | connectionString | string | PostgreSQL 连接串。 | | pool | Pool | 已存在的连接池实例。传入后会直接复用,不会由本包自动关闭。 | | poolConfig | PoolConfig | 细粒度连接池配置。 | | healthCheckQuery | string | 自定义数据库健康检查 SQL。 |

如果同时传了 poolconnectionString / poolConfig,会优先使用 pool

tile 常用子项

| 参数 | 类型 | 含义 | | --- | --- | --- | | extent | number | 瓦片渲染 extent。 | | buffer | number | 瓦片边缘缓冲区。 | | maxFeatures | number | 单次查询允许返回的最大要素数。 |

forRootAsync(...) 什么时候用

如果你的数据库连接或图层配置要从 ConfigService、远程配置或其他 provider 动态生成,使用 forRootAsync(...)

TileModule.forRootAsync({
  routePrefix: "api",
  inject: [ConfigService],
  useFactory: async (config: ConfigService) => ({
    layers: buildLayers(config),
    database: {
      connectionString: config.getOrThrow("DATABASE_URL"),
    },
  }),
})

forRootAsync(...) 下如果要配置统一前缀,请放在 TileModule.forRootAsync({...}) 的顶层 routePrefix 字段里,而不是放进 useFactory 返回值中。

默认行为与约束

  • 二维接口返回标准 MVT,三维接口返回自定义 3D PBF;参数集合尽量一致,但响应体不能互换。
  • routePrefix 只影响默认 Nest controller 的基础路径,不影响底层 core 引擎。
  • fields 不会绕过白名单,最终只会返回 propertyWhitelist 允许的字段。
  • filter 能力仍存在于底层引擎,但默认 HTTP controller 不会把它暴露成公开查询参数。
  • from/to 只有在图层配置了 timeField 后才可用。
  • minZoom/maxZoom 会与图层配置和模块默认值一起计算最终有效范围。
  • clusterEnabled=true 时必须配置 clusterMaxZoom
  • clusterFields 的输出别名不能与系统保留字段冲突。
  • gzip.enabled=true 时响应会带 Content-Encoding: gzip
  • 三维 cluster 的高度规则当前由 clusterAltitudeMode 控制,当前实现为 max
  • 若未显式关闭 controller,TileModule 默认会启用二维、三维、metadata、health 四类接口。
  • 内部创建的数据库连接池会在应用关闭时自动释放;外部传入的 database.pool 需要由调用方自己执行 pool.end()

常见接入方式

给 MapLibre 用二维瓦片

const source = {
  type: "vector",
  tiles: [
    "http://127.0.0.1:3000/tiles/data/{z}/{x}/{y}.pbf?crs=gcj02&fields=name,color"
  ]
};

给前端自定义三维加载器用

import { decodeTile } from "@deepinnet/geojson-to-mvt";

const response = await fetch(
  "http://127.0.0.1:3000/tiles-3d/data/8/209/111.pbf?crs=gcj02"
);
const buffer = await response.arrayBuffer();
const tile = decodeTile(new Uint8Array(buffer));

主要导出

运行时导出:

  • TileModule
  • TileHttpResponder
  • TileService
  • TILE_ENGINE
  • TILE_MODULE_OPTIONS

类型导出:

  • TileModuleOptions
  • TileModuleAsyncOptions
  • TileControllerOptions
  • TileControllersSelection
  • TileOptionsFactory

与 core 的关系

这个包只负责 NestJS 集成层。

所有底层能力都来自:

  • @deepinnet/service-mvt-middleware-core

包括:

  • 图层配置类型
  • 瓦片引擎
  • PostgreSQL adapter
  • filter / cluster / gzip 等能力

协议

本包按 Apache-2.0 协议分发。