
在我们的数字世界中,我们最宝贵的数据——从患者的病史到突破性的科学发现——常常被困在孤立的系统中,每个系统都说着自己的私有语言。这种碎片化阻碍了进步与合作。我们面临的挑战是找到一种通用语 (lingua franca),一套能让这些异构系统无缝通信的通用规则。表征状态转移 (REST) 架构风格为此提供了答案,它为构建稳健、可扩展且互联的系统提供了一个简单而强大的蓝图。它已成为打破数据孤岛、开启数字协作新时代的基础。
本文将深入探讨 RESTful API 的世界,探索使其如此高效的理念。在第一章“原则与机制”中,我们将剖析 REST 的核心概念,从作为契约的 API 理念,到统一接口与超媒体的精妙之处。随后,“应用与跨学科连接”一章将展示这些原则如何在现实世界中应用,通过创建数据可以自由安全流动的统一生态系统,彻底改变医疗健康和材料科学等领域。
要真正领会 RESTful API 的强大与优雅,我们不能从代码或端点开始,而必须从计算机科学的一个基本思想入手——这个思想几十年来一直塑造着我们构建稳健、持久软件的方式:契约的思想。
想象一下你正在盖房子,并雇佣了一位电气承包商。你与他们的合同很简单:你指定插座和开关的位置,他们保证当你插入标准电器或按下开关时,一切都会按预期工作。你不在乎他们使用什么牌子的电线,如何将电线穿过墙壁,或者使用什么工具。你只关心接口——即插座和开关——以及其承诺的行为。承包商可以自由创新,寻找更高效的材料,或改变他们的技术,只要他们遵守合同,你的房子就能完美运作。
一个设计良好的 RESTful API 正是这样一种契约。它在公共接口和私有实现之间划出了一条清晰的界线。这个思想与计算机科学中一个名为抽象数据类型 (ADT) 的概念直接对应。ADT 由其支持的操作(其接口)定义,而不是由其存储数据的方式(其实现)定义。REST API 为整个网络系统做了同样的事情。
服务器就像承包商一样,可以随时改变其内部工作方式。它可以从 SQL 数据库切换到 NoSQL 数据库,将其代码从 Python 重写为 Rust,或者将其服务器迁移到另一个大洲。只要它继续遵守公共 API 契约,依赖于它的无数客户端应用程序——移动应用、网站、其他服务——都不会崩溃。这种分离是构建能够历经多年演进而不因自身复杂性而崩溃的大规模分布式系统的秘诀。REST 的美就在于这份契约的简单与强大。
那么,这份契约是什么样的呢?它建立在几个简单而深刻的原则之上。
REST 的第一个原则是,不要将你的系统看作是过程的集合,而是看作“事物”的集合,我们称之为资源。资源是任何可以被命名和寻址的概念。它是你系统语言中的名词。在医疗健康系统中,Patient(患者)、Observation(观察,如血压读数)和 Encounter(就诊,如一次住院)都是资源。在材料科学中,AssetAdministrationShell(物理资产的数字孪生)及其 Submodels(描述如能耗等方面的子模型)都是资源。
至关重要的是,每个资源都有一个唯一、稳定的地址。这个地址就是它的统一资源标识符 (URI)。它就像那条特定信息的永久邮寄地址。例如,一个特定患者可能位于 /patients/12345,一个特定的材料子模型可能位于 /submodels/xyz-energy-v2。为万物命名的行为是整个系统的基础。
区分资源及其表征 (representation) 也很重要。资源是抽象概念——患者编号 12345。它的表征是当你访问其 URI 时得到的具体数据,可能是 JSON 或 XML 格式。服务器和客户端可以协商使用哪种表征,我们稍后会探讨这个过程。目前,关键思想是:我们通过操作资源的具体表征来与抽象资源进行交互。
这里我们触及了 REST 优雅之处的核心。我们不再为每个资源发明一套自定义的操作——比如 getPatientData()、createNewObservation()、updateSubmodelDetails()——而是对所有资源使用一小组固定的动词。这些动词是超文本传输协议 (HTTP) 的标准方法,也是 Web 的语言。这就是统一接口约束。
这是一个根本性的简化。这意味着,如果你知道如何与 RESTful 系统中的一个资源交互,你就掌握了与任何资源交互的基础知识。主要的动词有:
GET (读取): 检索资源的表征。向服务器请求 GET /patients/12345 会获取该患者的数据。GET 的一个关键属性是它是安全的,意味着它绝不能改变服务器上资源的状态。这就像阅读一份文档而不做任何标记。
POST (创建): 在一个集合中创建一个新资源。要添加一个新的血压读数,你会向 /observations 集合 POST 该读数的一个表征。然后服务器会创建该资源,为其分配一个新的唯一 ID(例如 /observations/9876),并告诉你它的位置。
PUT (更新/替换): 替换特定 URI 处资源的整个表征。如果你想更新患者 12345 的地址,你会向 /patients/12345 PUT 一个完整的、更新后的患者表征。
DELETE (删除): 移除一个资源。发送 DELETE /patients/12345 会删除该患者的记录。
这小组动词隐藏着一个超能力:幂等性 (idempotency) 的概念。一个操作如果执行多次与执行一次的效果相同,那么它就是幂等的。想象一下电梯里去 10 楼的按钮。按一次会呼叫电梯。再按十次也不会有任何新的效果;最终状态是相同的——电梯被安排去 10 楼。这是一个幂等操作。相比之下,拨动电灯开关不是幂等的;操作两次会撤销第一次的操作。
在网络这个不稳定的世界里,请求可能会超时,你也不确定你的消息是否被收到,幂等性就成了救星。PUT 和 DELETE 是幂等的。如果你发送一个 DELETE 请求后网络超时,你可以安全地再发一次。如果第一次成功了,第二次只会发现资源已经不在了,什么也不做。最终状态是相同的。然而,POST 通常是非幂等的。两次发送相同的 POST 请求很可能会创建两个相同的资源,这通常不是你想要的。
这个简单的区别决定了我们如何构建有弹性的客户端应用程序。对于必须能应对网络故障的操作,我们倾向于使用幂等动词。即使我们必须使用 POST,对契约的巧妙扩展,比如 FHIR 中的 If-None-Exist 头,也可以使创建操作在效果上变为幂等的,防止重试时创建重复的资源。这是 API 契约提供强大保证的又一个例子。如果条件不满足,服务器不会猜测;它会返回一个精确的错误,比如 412 Precondition Failed 状态码,使系统的行为完全可预测。
最后几块拼图确保了客户端和服务器可以清晰地沟通并独立演进。
首先,消息必须是自描述的。一条消息应自身携带足够的信息,以便接收方理解它是什么以及如何处理它。这通常通过内容协商 (content negotiation) 实现。客户端可以通过发送 Accept 头(例如 Accept: application/fhir+json)来声明它理解哪种表征。服务器随后选择一个合适的格式,并在 Content-Type 头中声明它返回的内容类型。
当 API 需要演进时,这个机制变得至关重要。想象一下,一个服务器想要为其 Patient 资源引入一个新的、向后不兼容的版本。它如何能在不破坏所有期望旧格式的老客户端的情况下做到这一点?答案是让版本成为契约的一部分。新客户端可以请求新版本 (Accept: application/fhir+json; fhirVersion=5.0.0),而老客户端则继续请求旧版本(或不指定版本,服务器会在一段时间内默认为旧版本)。服务器可以维护一个内部的规范模型,并仅在响应时将其动态转换为所请求的表征。这使得系统能够优雅地演进,无需停机即可同时支持多个客户端版本。
第二个,或许也是最美的原则是作为应用状态引擎的超媒体 (HATEOAS)。这是一种花哨的说法,意思是资源的表征不应只包含数据,还应包含指向其他相关资源和操作的链接。
以 FHIR 的 Observation 资源为例。它不只是说患者的 ID 是 "12345"。它包含一个引用 (Reference),这是一个超链接:"subject": {"reference": "Patient/12345"}。客户端应用程序不需要硬编码这样的知识:要获取患者详细信息,必须构造一个像 /Patient/{id} 这样的 URL。相反,它只需跟随 Observation 表征中提供的链接。
这将客户端与服务器特定的 URI 结构解耦。服务器可以自由地重新安排其 URL,只要它在响应中提供正确的链接,客户端就会继续正常工作。这使得与 API 的交互变成了一种发现行为,很像浏览网页。你从一个点开始,通过跟随链接导航到其他点。这种可发现性通过诸如服务器的 CapabilityStatement 之类的机制得到进一步增强,这是一个特殊的资源,它像一张地图,准确地告诉客户端它支持哪些资源、交互和搜索功能。
有了这些简单的原则——资源、统一接口以及自描述的、由超媒体驱动的消息——我们就可以构建出惊人复杂且强大的交互。
复杂查询: 你如何请求一个材料数据库提供“所有最多含有 8 个原子位点的二元锂氧化物”?你不需要一个自定义的端点。你使用标准的 /structures 资源,并在 URI 后面附加一个强大的过滤字符串。像 filter=elements HAS ALL 'Li','O' AND nelements = 2 AND nsites = 8 这样的查询允许极其具体的请求,同时仍然在对资源集合使用简单的 GET 动词的框架内操作。
原子事务: 你如何确保一个包含多个资源变更的订单——例如,一次性创建多个 MedicationRequest——是原子的(作为一个整体成功或失败)?你不需要复杂的会话管理。你将所有独立的资源变更组装成一个类型为 transaction 的 Bundle 资源,然后将这个单一的 bundle POST 到服务器。transaction bundle 的契约保证了原子性。复杂性被封装在一个定义良好的表征中,而不是在交互本身。
何时变通规则: REST 不是教条。有时,所需的功能确实是一个计算或过程,而不是对资源的简单操作。例如,一个请求要验证数百个 MedicationAdministration 和 MedicationRequest 资源之间的一致性,并返回一个聚合摘要,这并非一个简单的 GET。对于这些情况,REST 提供了一个结构化的应急方案:一个自定义操作 (operation)。但这并非一个随意的函数调用。它在一个 OperationDefinition 资源中被正式定义,这使得该操作是可发现的,并清晰地指定了其输入、输出和行为——例如它是否是只读的 (affectsState=false)。这是对契约的扩展,而不是对其的违背。
从一个简单的契约到一个互联数据的宇宙,REST 的原则引导我们构建的系统不仅功能强大,而且简单、有弹性,最重要的是,经久耐用。
想象一下,你试图建立一个全球图书馆,但每本书都用不同的语言、不同的字母书写,并按照不同的系统上架。巴黎的一位研究员可能有一个绝妙的见解,但东京的同事却无法在此基础上继续研究,因为他们甚至找不到那本书,更不用说阅读它了。这不仅仅是一个比喻;几十年来,这就是数字世界的现实。我们最宝贵的数据——从患者的病史到突破性科学模拟的结果——都被锁在数字孤岛中,每个孤岛都说着自己的私有语言。我们如何让这些异构系统相互对话?我们需要一种通用语,一种数据的共同语言。或者,更准确地说,我们需要一套通用的语法规则,让任何系统都能理解任何其他系统。这就是表征状态转移(REST)架构风格所扮演的深刻且改变世界的角色。
数据碎片化的挑战在医疗健康领域比任何地方都更为关键。单个患者的故事散布在十几个系统中:医院的电子健康记录 (EHR)、专科医生的诊所、药房、影像中心和基因检测实验室。每个系统都掌握着谜题的关键一块,但它们很少相互交流。很长一段时间里,解决方案都笨拙而僵化。一种方法是事件驱动的消息传递(如经典的 HL7 V2 标准),这就像在系统之间连续发送电报:“患者入院”、“化验结果就绪”。另一种是文档为中心的交换,即将患者记录的快照打包成一个大型静态文档(如临床文档架构 CDA 文件)并发送出去。 每种方法都有其用武之地,但如果手机上的一个移动应用只想知道一件事:“我目前的用药是什么?” 发送整个文档是小题大做,而接入医院事件流则不切实际。
这正是 REST 的优雅之处,特别是在快速医疗互操作性资源 (FHIR) 标准中大放异彩。FHIR 并未将数据视为事件流或庞大的文档,而是基于 REST 原则,指出所有医疗健康信息都可以分解为离散的“资源”:一个 Patient 资源、一个 Medication 资源、一个用于单个化验测试的 Observation 资源。每个资源都有一个可预测的地址 (URL),并且可以通过一小组统一的动词——我们熟悉的 Web 的 GET、POST、PUT、DELETE——来操作。突然之间,构建那个患者应用的开发者不再需要理解十个不同 EHR 系统的复杂内部结构。他们只需发出一个简单的、安全的 Web 请求:“GET 这个 Patient 的 Medication 资源。” 这个简单的想法正在彻底改变医疗健康行业。
让我们看得更深一些。考虑基因组数据的爆炸式增长。一个临床实验室可能会对一个肿瘤进行测序,并生成一个变异调用格式 (VCF) 文件,这是一个高度技术性的基因突变列表。这个文件对生物信息学家至关重要,但对 EHR 和肿瘤科医生来说却是天书。为了使其具有临床实用性,必须对原始数据进行转换。使用像 HL7 FHIR Genomics 这样的 RESTful 方法,原始的 VCF 数据被映射到一组可互操作的 Observation 资源中。每个变异都成为一个资源,直接链接到 Patient、它来自的 Specimen(标本)以及包含临床解读的 DiagnosticReport(诊断报告)。它不再只是一个文本文件中神秘的一行;它成为患者故事的一部分,任何懂 FHIR 的系统都能理解。
同样的原则使我们能够查询全世界的集体生物学知识。像 Ensembl(编目基因及其版本)和 Online Mendelian Inheritance in Man (OMIM)(编目基因与疾病关系)这样的大型数据库,都通过 REST API 开放其数据。一个临床流程可以编程方式地询问 Ensembl:“这个特定转录本标识符的 DNA 序列是什么?”(通过像 /sequence/id/:id 这样的端点),或者询问 OMIM:“这个基因已知的等位基因变异有哪些?”(使用 /api/entry 并带上 include=allelicVariant 参数)。API 设计本身反映了底层的生物学和数据组织,允许研究人员通过简单、可预测的调用来导航复杂的信息。
同样的统一也正在药物和医学影像领域发生。像“赖诺普利 10mg”这样的名称可能含糊不清。它是片剂吗?它的标准代码是什么?由美国国家医学图书馆提供的 RxNorm API 充当了权威的翻译器。通过简单的 REST 调用,一个系统可以将一个混乱的药物字符串解析为一个标准的 RxNorm 概念唯一标识符 (RxCUI),检查拼写错误的名称的近似匹配,或者展开一个治疗类别以找到其所有成员药物。 同样,长期由一个名为 DIMSE 的复杂、有状态协议主导的医学影像世界,正在被 DICOMweb 现代化。像 QIDO-RS(用于查询)、WADO-RS(用于检索)和 STOW-RS(用于存储)等服务是使用标准 Web 协议 (HTTPS) 的 RESTful 接口。这不仅使编写与影像档案交互的软件变得更加容易,而且还简化了安全和网络管理,因为它使用的是与 Web 其他部分相同的 443 端口。现在,研究人员可以只检索 MRI 的单帧图像或放射科医生的注释,而无需下载整个庞大的研究文件。
一个设计良好的 REST API 不仅仅是获取数据的一种方式;它是一个可以构建整个生态系统的基础。简单、统一接口的美妙之处在于它鼓励扩展和组合。
FHIR 标准就是一个完美的例子。它本身为医疗健康数据提供了“名词”(资源)和“动词”(HTTP 方法)。但在它之上构建的一系列规范则提供了“故事”。SMART on FHIR 规范定义了第三方应用程序如何从 EHR 内部安全启动,获得对患者数据的授权、范围有限的访问。这就是你能够将自己喜欢的健康应用连接到医院门户网站的原因。Bulk FHIR 规范提供了一种为人口健康研究和 AI 模型训练高效导出数百万患者数据的方法。而 CDS Hooks 则定义了一种方式,让 EHR 在工作流程的关键时刻——例如,当医生即将签署医嘱时——调用外部 AI 服务,并以包含建议和操作的“卡片”形式接收实时的临床决策支持。 这些不是分离、脱节的技术;它们是建立在同一个 RESTful 基础之上的层次,各自解决不同的问题。
这些生态系统的演进也推动了简单请求-响应模型的边界。在临床试验中,中央管理系统需要立即知道远程站点何时输入了新数据。一种方法是轮询:中央系统反复询问远程系统的 REST API,“有新东西吗?有新东西吗?有新东西吗?” 这种方式效率低下,尤其是在有数百个站点、数千名受试者的情况下,可能会使 API 不堪重负。一个更优雅、事件驱动的解决方案是 webhook。当事件发生时,远程系统会主动向中央系统预先注册的地址发送一个小通知——一个 RESTful 的 POST 请求。这种基于推送的模型效率极高,并提供了现代临床运营所需的近乎实时的更新。这是一个绝佳的例子,说明了 REST 范式如何能够适应支持拉取式和推送式两种通信模式。
这些思想的力量并不仅限于医疗健康。任何产生大量分布式数据的科学领域都面临着同样的碎片化挑战。以计算材料科学为例。世界各地的研究小组运行复杂的模拟来预测新材料的特性,产生了 PB 级的数据。多年来,这些数据被锁定在本地服务器中,其格式遵循着几十种不同模拟代码的约定。
解决方案?与医疗健康领域完全相同:一个通用的、RESTful 的 API 规范。材料设计开放数据库集成 (OPTIMADE) 标准定义了一种查询材料科学数据库的通用方法。它规定了一个通用的 RESTful 接口、一种过滤语言,以及像 chemical_formula_descriptive 这样属性的标准化名称。现在,一位科学家可以编写一个脚本,在几十个不同的数据库中搜索,比如说,所有带隙大于 eV 的非磁性绝缘体,而无需了解其中任何一个数据库的内部工作原理。 这极大地加快了材料发现的步伐,为设计从更好的太阳能电池到新型合金的一切事物提供了数据驱动的方法。这是一个强有力的证明,证明 REST 不仅仅是构建网站的技术,而是一种促成大规模科学合作的基本模式。它正是我们全球图书馆所缺失的通用语法。
从追踪单个患者的基因突变到在全球范围内寻找新材料,应用多种多样,但其基本原则是统一的,并且美在其简单性。通过将复杂数据视为一组可寻址的资源,并定义一小组统一的操作与之交互,RESTful 架构风格提供了一个稳健、可扩展且异常灵活的框架。它驯服了分布式信息的混乱,创造了一个数据可以自由安全流动的世界,不仅跨越了网络,也跨越了学科的界限,以我们才刚刚开始探索的方式加速发现并连接知识。