GithubHelp home page GithubHelp logo

famousquotesapi's Introduction

Famous Quotes API

This project was part of a 35-hour challenge, along with a corresponding WP plugin, designed to see what I can do in a small amount of time. This portion required that I use Symfony Framework and Doctrine; two things that I've no previous experience of.

What we have here is a simple API for CRUD operations, intending to be used for creating, retrieving, updating and deleting famous quotes and their authors.

Authors (is that the right term?) can be created without having any quotes. In fact, it's required that you create an author before you can record any quotes for them. Quotes cannot have no author.

This code should exist on Github at Antnee/famousquotesapi, and the accompanying WordPress plugin at Antnee/famousquoteswp. I rarely leave any code publicly available, so don't expect these repositories to exist forever.

Conventions

IDs

All IDs are UUID v4 and generated by the Ramsey/UUID library. I did not use auto-incrementing integer values because they're just really bad practice. UUIDs can have major performance issues when used as the primary key in MySQL, and if I expected this API to get serious usage I would have implemented ordered UUIDs. Likewise, I should really have made the ID fields binary or int, but as I wasn't too sure about how Doctrine would handle it all, I decided to simplify and use a string type (varchar in MySQL). In production, this is bad. Don't do this.

Message Structure

All success messages will be returned in the following format:

{
  "requestId": "12345678-90ab-cdef-g123-4567890abcde",
  "time": 1234567890,
  "error": null,
  "content": {
    "foo": "bar"
  }
}

The content may be an array where more than one item will be returned, an object where there is a single item to return, or a boolean value in the case of a delete operation.

Note that success does not mean a 200 HTTP status code every time. Where a new resource is created you will receive a 201 instead.

Where there is an actual error, the response will look like this:

{
    "requestId": "12345678-90ab-cdef-g123-4567890abcde",
    "time": 1234567890,
    "error": {
        "code": 1234,
        "message": "Error Message"
    },
    "content": null
}

Errors may come as 400, 401 and 404 HTTP status codes. You should never receive a 403 as once you're authorised, you can do anything. No roles or permissions here.

You should never find that both error and content are both null. Nor should it be possible for them to both not be null at the same time. If this happens then you've found a bug.

Authentication

Some simple authentication is included. Namely, you must provide an x-api-key header with every request, regardless of endpoint and method. If you are authenticated then you can perform any actions.

The API keys are currently hard-coded into /src/Security/ApiKeyUserProvider.php and the bundled key is simply fhRBi4atT9xcLlQBJMz7lRDH1HL480shLdYlfmuPulQ. You can add more if you like, and you can remove that one, but that's how it's bundled for now. DB/Doctrine is used for the Author and Quote entities; I didn't feel it necessary to use a DB connection for this. However, it would be fairly trivial to add a DB based provider if required.

Endpoints:

Here follows a list of available endpoints, simple examples of how to use them, and what the response should look like.

GET /

Redirects to /ping

GET /ping

Example

GET http://localhost/

{
    "requestId": "344255f8-4832-4574-8315-eae5fb147151",
    "time": 1234567890,
    "error": null,
    "content": {
        "ack": 1234567890
    }
}

GET /quotes

Redirects to /quotes/random

GET /quotes/random

Returns a random quote

Example:

GET http://localhost/quotes/random

{
    "requestId": "1e440c1b-0801-4f27-9a06-b10a8abea174",
    "time": 1234567890,
    "error": null,
    "content": {
        "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
        "text": "If at first you don't succeed, try, try again",
        "author": {
            "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
            "name": "William Edward Hickson",
            "quotes": 1
        }
    }
}

GET /quotes/{id}

Returns a specific quote

Example:

GET http://localhost/quotes/51f74f6e-8e20-42ec-ba21-ac3ae62658ef

{
    "requestId": "1e440c1b-0801-4f27-9a06-b10a8abea174",
    "time": 1234567890,
    "error": null,
    "content": {
        "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
        "text": "If at first you don't succeed, try, try again",
        "author": {
            "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
            "name": "William Edward Hickson",
            "quotes": 1
        }
    }
}

DELETE /quotes/{id}

Deletes a given quote ID

Example

DELETE http://localhost/quotes/51f74f6e-8e20-42ec-ba21-ac3ae62658ef

{
    "requestId": "8861da06-e2ac-4fdc-b521-81f6a8d72836",
    "time": 1234567890,
    "error": null,
    "content": true
}

Notice the true boolean value in content indicating success

PATCH /quotes/{id}

Updates the quote text, the author ID, or both (making this similar to a PUT)

Example

PATCH http://localhost/quotes/ea95b2d1-504f-40e6-8736-36b0b752dca1

Body
{
    "text": "It's one small step for man, one giant leap for mankind",
    "authorId": "b16d3b31-3bda-49a4-a661-50280a0c50ca"
}
Response
{
    "requestId": "ff782c32-46d7-403e-abd8-d578a5fdb087",
    "time": 1234567890,
    "error": null,
    "content": {
        "id": "75c07c54-706a-4c4e-9a10-3435dcf035bb",
        "text": "It's one small step for man, one giant leap for mankind",
        "author": {
            "id": "b16d3b31-3bda-49a4-a661-50280a0c50ca",
            "name": "Neil Armstrong",
            "quotes": 1
        }
    }
}

PUT /quotes/{id}

Similar to PATCH /quotes/{id}, except you must pass a full, valid quote object (excluding the ID, which is obtained from the URI), which will completely replace the quote at this ID

GET /authors

Returns all authors

Example:

GET http://localhost/authors

{
    "requestId": "fb8fb9e6-83fd-4b22-bc7c-8fb91036bed7",
    "time": 1234567890,
    "error": null,
    "content": [
        [
            {
                "id": "31d14cc5-93c7-4f37-bd54-0b987b0a9acf",
                "name": "Robert Kennedy",
                "quotes": 3
            },
            {
                "id": "32e98bce-472c-4b1c-989c-7911e04a17ed",
                "name": "will.i.am",
                "quotes": 1
            },
            {
                "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
                "name": "William Edward Hickson",
                "quotes": 1
            },
            {
                "id": "87e7106b-2d2b-4a5d-9a72-882e2c7f8acb",
                "name": "Johny Five",
                "quotes": 1
            },
            {
                "id": "b16d3b31-3bda-49a4-a661-50280a0c50ca",
                "name": "Neil Armstrong",
                "quotes": 1
            }
        ]
    ]
}

POST /authors

Create a new author

Example

POST http://localhost/authors

Body
{
    "name": "Neil Armstrong"
}
Response
{
    "requestId": "8e201ea7-f0ac-4bf5-8c17-e22191cd0086",
    "time": 1234567890,
    "error": null,
    "content": {
        "id": "b16d3b31-3bda-49a4-a661-50280a0c50ca",
        "name": "Neil Armstrong",
        "quotes": 0
    }
}

GET /authors/{name}

Returns details about the named author

Example:

GET http://localhost/authors/william%20edward%20hickson

{
    "requestId": "3484a639-0eed-4185-b4c2-b9e69825eb77",
    "time": 1234567890,
    "error": null,
    "content": [
        {
            "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
            "name": "William Edward Hickson",
            "quotes": 1
        }
    ]
}

DELETE /authors/{name}

Deletes an author, and their corresponding quotes

Example

DELETE http://localhost/authors/neil%20armstrong

{
    "requestId": "0505ed2b-8cad-44c1-ac3e-560ea8370c0b",
    "time": 1234567890,
    "error": null,
    "content": true
}

Notice the true boolean value in content indicating success

PATCH /authors/{name}

Patches the author's name. Nothing else can be patched. Consequently, a PATCH and a PUT are identical at this endpoint

Example:

PATCH http://localhost/authors/neil%20armstrong

Body
{
    "name": "Buzz Aldrin"
}
Response
{
    "requestId": "2fa9d45e-4896-4e65-867a-387faaa03e8b",
    "time": 1234567890,
    "error": null,
    "content": {
        "id": "860970db-621f-402b-b286-0861b7bd5dfd",
        "name": "Buzz Aldrin",
        "quotes": 0
    }
}

Note that if we now GET /authors/buzz%20aldrin/quotes we now see this famous quote as attributed to Buzz, instead:

{
    "requestId": "50cc8fe3-7d53-4ce5-b615-104a18071978",
    "time": 123456789,
    "error": null,
    "content": [
        {
            "id": "468dcbe9-369c-4605-b62f-485f1d122159",
            "text": "It's one small step for man, one giant leap for mankind",
            "author": {
                "id": "53801e48-bfdd-46e0-a982-a888a5e90243",
                "name": "Buzz Aldrin",
                "quotes": 1
            }
        }
    ]
}

PUT /authors/{name}

See PATCH /authors/{name}

GET /authors/{name}/quotes

Gets all quotes by this author

Example:

GET http://localhost/authors/william%20edward%20hickson/quotes

{
    "requestId": "cb7fdd33-db4d-4169-a724-e30b4424ff25",
    "time": 1234567890,
    "error": null,
    "content": [
        {
            "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
            "text": "If at first you don't succeed, try, try again",
            "author": {
                "id": "51f74f6e-8e20-42ec-ba21-ac3ae62658ef",
                "name": "William Edward Hickson",
                "quotes": 1
            }
        }
    ]
}

Exceptions

The following Exceptions exist in the application. You may not see all of these in your responses, but they're documented here for completion:

Code Exception Explanation
1001 NoAuthorsFoundException No authors exist in the database at all
1002 AuthorIdNotFoundException An author ID was provided, but it did not match a known author
1003 AuthorNameNotFoundException Am author name was provided, but it did not match a known author
1011 AuthorDataInvalidException The author data provided did not match the required specification
1102 AuthorNotAddedException The author that you tried to insert could not be added
1103 AuthorNotUpdatedException The author record could not be updated
1104 AuthorNotDeletedException The author record could not be removed
1201 AuthorQuoteCountNotZeroException The author has more than zero quotes. Can occur during an author delete scenario
2001 NoQuotesFoundException No quotes exist in the database at all
2011 QuoteDataInvalidException The quote data provided did not match the required specification
2102 QuoteNotAddedException The quote that you tried to insert could not be added
9001 InvalidApiKeyException The provided API key was not recognised

Known bugs, quirks and other points

  • If you request quotes for an unknown author (eg GET /authors/doesnotexist/quotes) you will receive an empty array in response, and not receive a 1003 unrecognised author error
  • DELETE operations return a boolean value, rather than an error. I'm tempted to go back and do this properly, but that would defeat the purpose of this challenge. Just be aware that I know it's inconsistent and not a great design
  • You cannot GET at /authors/{name}/quotes/{id}. Nor can you DELETE, PATCH or PUT. After you've sent a POST to /authors/{name}/quotes you must retrieve the quote by performing a GET on /quotes/{id}. With more time I would probably allow these methods at these endpoints as it seems somewhat obvious and a sort of filter (ie only get this quote ID if this is the author's name), but it's far from necessary in this timeframe
  • There is no pagination. I don't expect that there'll be enough data in here to justify the extra logic at both ends. For a larger solution it would be necessary
  • There is no validation, either. The closest that we get to this is that Doctrine won't allow invalid data to be inserted into the database. But other than that there is nothing. This clearly will not wash in a production system
  • The authentication system works fine when you give correct credentials, however there is presently no handling for invalid (or missing) credentials, so you will see a default 500 exception from Symfony. This shouldn't be too difficult to resolve, but not having any experience oif Symfony at all prior to a few days ago, I elected to leave it for now. I am aware that it's a problem, however
  • I haven't implemented HATEOAS or similar. This is not a model API. There are a few architectural things that I'm not pleased about. Symfony may have the answer already, or I could optimise my code, but that's not the point of a) this challenge or b) creating a prototype
  • Each quote is available to all API callers. There is no pool per user, so if one API consumer deleted a quote, it would be replicated across all consumers. The theory here is that this is your API, and you are the consumer

Tests

Where are the tests, Anthony? I would usually write tests first, then build my application around these tests. However, because I needed to learn both Symfony and Doctrine as I went, I needed to build first, test later. I made the judgement that I should skip tests. I was very thorough in my previous challenge so I didn't feel that I needed to prove anything while learning two new major elements at the same time.

I have however included a Postman collection dump for the Quotes and Author collections that I've been using for my testing in /tests/postman which can be imported for manual testing. It's not the same thing, at all, but it helps understand and to aid integration.

famousquotesapi's People

Contributors

antnee avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.