Open-source project
thundercomb/poetrydb avatar
thundercomb/poetrydb

PoetryDB: a URL-shaped API for searching public domain verse

The Internet's first Poetry API

661 stars46 forksRubyGPL-2.0

At a glance

What is it?
PoetryDB is a Sinatra API over a MongoDB corpus of public domain poems, where every query is expressed in the URL path rather than a query string. It is easy to call, it exposes search over authors, titles and lines, and it is one of the older free poem APIs still running on the public internet.
Who is it for?
Adopt PoetryDB when you are building something that needs to search a body of public domain verse by author, title, line count or phrase, and you are happy to express the query in the URL. Do not adopt it for licensed or modern poetry, since the corpus is built from editions such as Project Gutenberg texts, and do not treat it as a stable contract, because there are no tagged releases and the format is expressive enough to change quietly.
Can I use it commercially?
Yes, with conditions. GPL-2.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole query lives in the path, not the query string

Most HTTP APIs take a fixed path and put the parameters in a query string or a request body. PoetryDB inverts that. The general format of every route is:

code
/<input field>/<search term>[;<search term>][..][:<search type>][/<output field>][,<output field>][..][.<format>]

That single line is the entire interface, and it is worth reading slowly because every optional part is visible in it. The input field names what you are searching. The search term is what you are searching for, and several terms separated by semicolons are allowed. A colon introduces a search type, a slash introduces a comma separated list of output fields, and a dot introduces the response format.

A concrete call looks like this:

code
https://poetrydb.org/title/Ozymandias/lines.json

which returns the lines of the poem, in JSON, as an array with a single object:

code
[
  {
    "lines": [
      "I met a traveller from an antique land",
      "Who said: \"Two vast and trunkless legs of stone",
      "Stand in the desert. Near them on the sand,",

The design has a real consequence. Because the query is in the path, the API is trivially cacheable and linkable, and a search is a URL you can paste into a browser and read. The cost is that the same design gives you no way to add a parameter the author did not think of, and the grammar does not grow gracefully.

For a small, well-defined corpus, that trade is a good one. The grammar is small enough to hold in your head, and the responses are plain data with no envelope to unwrap.

Search fields, search types and the union rule

The input field can be one of six things: author, title, lines, linecount, poemcount and random. The first three match text, linecount matches the number of lines in a poem, and the last two control how many results come back.

The linecount definition has a precision that is worth noting because it is easy to get wrong. The number of lines includes section headings but excludes empty lines, such as section breaks. So a poem printed with a heading and stanza breaks does not have the line count you would count by eye.

The two count fields are mutually exclusive. poemcount is the number of poems to return and cannot be combined with random, and random is the number of random poems to return and cannot be combined with poemcount. That is a sensible guard against an ambiguous request, and it is enforced by the API rather than by convention.

The search type is optional and has three values. The default matches the search term with any part of the input field, so a partial title works. The :abs type matches the term exactly, the whole field. The :word type matches the term as a whole word within the field, and the documentation gives the case that distinguishes it: eleven matches eleven but not eleventh.

Then there is the union rule, which is the part of the reference that most people discover late. When several input fields are supplied but only a single search term, the term is matched as a union across those fields. The example given is /title,lines/roland, which returns poems where roland appears in the title or in the lines, the union of /title/roland and /lines/roland. Union search applies to the author, title and lines fields. When the number of search terms equals the number of input fields, the fields are combined as an intersection instead.

That is the kind of detail that turns a query language from a list of features into something you can reason about.

Output fields, and the source field that only sometimes exists

The output field list is optional and comma delimited. You can ask for author, title, lines, linecount or source, or for several of them together, and the default is to return all data of each matching poem.

The source field is the interesting one. It returns the source of each of the matching poems, which the documentation describes as the edition the text was taken from, a name and a URL, for example a Project Gutenberg ebook. The critical qualifier is in the same line: it is only present for poems whose provenance has been recorded.

That qualifier has practical weight for anyone using the API for quotation or attribution. Some poems in the corpus will tell you exactly which edition the text came from, and others will not tell you anything. If your use depends on knowing the source, you cannot assume the field is populated, and the default output does include source where it is recorded, so the safest approach is to request it explicitly and handle its absence.

The format is the last piece of the path. The suffix .json returns data in JSON format and .text returns it in text format, and the default with no suffix is JSON.

One more rule closes the grammar: the notation means that by using the same syntax, more instances of the preceding type can be included. In practice that is what the semicolon separated search terms and the comma separated output fields already express, and the reference document is truncated mid-sentence on that point, so the exact repeated-parameter semantics are not spelled out in full in the text available.

The sonnet generator that shows what the API is for

The README makes the case for the API with a small Ruby program, and the program is more informative than any feature list would be.

ruby
require 'httparty'

response = HTTParty.get("https://poetrydb.org/author,linecount/Shakespeare;14/lines").to_a
(0..13).each { |i| puts response[rand(154)]['lines'][i] }

Read the URL carefully, because it exercises most of the grammar in one request. The input fields are author and linecount. The search terms are Shakespeare and 14, separated by a semicolon, and since the number of terms equals the number of fields, they combine as an intersection: poems by Shakespeare with fourteen lines. The output field is lines.

The program then picks a random poem from the returned set and prints one line from it, fourteen times, once per index. The result is a sonnet assembled from real lines by different poems.

The documentation frames this as a step towards making your own version of Raymond Queneau's One Hundred Thousand Billion Sonnets, but based on lines from English language verse rather than from a combinatorial system. That comparison is the point of the whole exercise: the API turns a body of poetry into input a program can rearrange, and the randomness comes free from the response set.

The output shown in the documentation is a fourteen line poem that reads as a coherent sonnet, which is a good advertisement for the corpus.

It also shows the pragmatic shape of the API. You ask for poems matching a structured filter, you get back a list, and you treat it as data. There is no template mechanism, no generation endpoint and no state on the server.

Sinatra, MongoDB, and a database you are not allowed to query directly

The architecture section is brief but decisive. The API is written in Ruby and uses Sinatra to resolve API routes, the poetry data is stored in a MongoDB database, and the Ruby code is provided as open source.

The important sentence is the last one: the PoetryDB database is not directly accessible, in order to preserve its integrity. So the public instance is a read only API over a curated corpus, and the MongoDB behind it is not something you can query with your own client. The repository gives you the server code, not the data.

That distinction matters if you are evaluating the project for your own use. The code and the data are separate assets with separate licences, and the repository root contains both LICENSE.txt for the code and a separate DATA_LICENSE.md, which is the arrangement you would expect when the corpus of poems carries its own terms distinct from the code that serves it.

The deployment artefacts are conventional Docker Compose. The compose file defines a mongo service running the mongo:4.2.9 image with a named volume at ./mongo-volume, an init script mounted into the container entrypoint directory, and a poetrydb service built from the local Dockerfile. The app service gets its connection details through environment variables, MONGODB_URI pointing at the mongo host on port 27017, along with MONGODB_USER and MONGODB_PASS.

The app container command populates the database and then starts the server:

yaml
    command: bash -c "ruby /poetrydb/tools/db_populate.rb /poetrydb/tools/test.json &&
                      ruby /poetrydb/tools/db_populate_authors.rb /poetrydb/tools/test_authors.json &&
                      bundle exec unicorn -p 3000 -c unicorn.rb"

So the local setup runs on port 3000 behind unicorn, and the population scripts take JSON input files, one for poems and one for authors. The Dockerfile is short: it starts from ruby:3.3.9, copies the project into /poetrydb, works in the app subdirectory and runs bundle install. The repository also holds a backlog.txt, a test directory, a tools directory and an Architecture_Diagram file in both Dia and JPG form.

What to expect from a project without a release history

There are no GitHub releases for this repository, and the version in use on the public site is not tied to a tag you can pin. The last push was on 2026-09-30.

That is worth weighing if you are building on the API. The route grammar is expressive, and expressive grammars grow. A new output field or a change in matching behaviour can appear without a version marker, and the documentation that describes the grammar is a README rather than a versioned specification.

The mitigating factor is the same thing that makes the API pleasant: the surface is small. Six input fields, three search types, a handful of output fields and two formats. A small surface has fewer places for a quiet breaking change to hide, and if you pin your requests to the fields you actually use, a change to behaviour elsewhere is unlikely to reach you.

The licence situation is also split in a way to be aware of. The code is under GPL-2.0, and the corpus sits under its own data licence in DATA_LICENSE.md. If you are redistributing scraped poems rather than calling the API, you are in the data licence's territory, not the code licence's, and those are different questions.

The homepage is poetrydb.org, and the project describes itself as the Internet's first Poetry API. The stated motivation is worth noting as a design constraint on the whole thing: the author wanted to give poets and writers the same programmatic access to language that other people have, and that is why the search operates on lines as well as titles and authors. Searching by phrase inside the text of a poem is the feature that a title and author search would not give you.

Editorial conclusion

Adopt PoetryDB when you are building something that needs to search a body of public domain verse by author, title, line count or phrase, and you are happy to express the query in the URL. Do not adopt it for licensed or modern poetry, since the corpus is built from editions such as Project Gutenberg texts, and do not treat it as a stable contract, because there are no tagged releases and the format is expressive enough to change quietly. Before you build on it, check the format section of the API reference for the output field list, and verify the source field on any poem where provenance matters, because it is only present where provenance was recorded.

Frequently asked questions

How do I search for a poem by title with the PoetryDB API?

Put the field and the term in the path, for example https://poetrydb.org/title/Ozymandias/lines.json. The title field matches the title or part of the title, the lines output field returns the lines, and the .json suffix requests JSON, which is also the default format.

What search types does PoetryDB support?

Three. The default matches the term with any part of the field, :abs matches the term exactly against the whole field, and :word matches the term as a whole word, so eleven matches eleven but not eleventh.

Can I search PoetryDB by a phrase inside the lines of a poem?

Yes, the lines field matches part of a line or lines of a poem, and it can be combined with title or author in a union when a single search term is given across several fields.

What does the source field in PoetryDB return?

It returns the source of each matching poem, meaning the edition the text was taken from, a name and a URL such as a Project Gutenberg ebook. It is only present for poems whose provenance has been recorded, so it can be absent.

Can I access the PoetryDB MongoDB database directly?

No. The database is not directly accessible in order to preserve its integrity. The repository provides the Ruby and Sinatra server code as open source under GPL-2.0, while the poem corpus is covered separately in DATA_LICENSE.md.

Official sources

  1. Issues
  2. License: GPL-2.0
  3. Project website
  4. README
  5. thundercomb/poetrydb on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thundercomb-poetrydb.svg)](https://hysenlabs.com/projects/thundercomb-poetrydb)

Was this useful?