This API / backend is a service to store the data of the Wankul trading card game by Wankil Studio. The cards are not mine and are the exclusive property of Wankil Studio. If you're interested in buying theses cards, you can do it here.
To use this API / backend, you need to have Node JS installed.
Then, simply clone this repository in the directory of your choice with
git clone [email protected]:Timeuh/Wankul-API.git
And install the node dependencies with
npm install
You must also have a .env
file with the following properties in order for the program to run :
Key | Value |
---|---|
DATABASE_URL | Your database URL (I use Neon, a Postgres database) |
DIRECT_URL | Another key for Neon database |
JWT_SECRET | Secret to encode your jwt with |
TOKEN_HEADER | Request header to verify your token |
To generate and access the database, I use Prisma ORM.
All tables are declared in prisma/schema.prisma
file.
You can modify if you want but the architecture is already functional.
If your database is created (empty, no tables) you can run the following command :
npx prisma migrate dev --name init
It will create the tables declared in schema file, and a SQL migration file.
Due to the insertion of any new entity being tied to authentication with a jwt, I recommend to create a new user before any other entity (type, description, card, artist, etc).
Once you did all of that, you're ready to go !
To run the app, simply execute the command
npm run server
This will start the Node server and watch for every change in the code. The server will be available at the address
API routes for artists
/api/artist/:id
{
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"links": {
"self": "/api/artist/1",
"all": "/api/artist/",
"cards": "/api/artist/1/cards"
}
}
/api/artist/
{
"type": "collection",
"length": 2,
"artists": [
{
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"links": {
"self": "/api/artist/1",
"all": "/api/artist/",
"cards": "/api/artist/1/cards"
}
},
{
"artist": {
"id": 2,
"name": "Ben Giletti"
},
"links": {
"self": "/api/artist/2",
"all": "/api/artist/",
"cards": "/api/artist/2/cards"
}
}
]
}
/api/artist/:id/cards
{
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"links": {
"self": "/api/artist/1",
"all": "/api/artist/",
"cards": "/api/artist/1/cards"
},
"cards": {
"type": "collection",
"length": 2,
"cards": [
{
"card": {
"id": 1,
"name": "Navire Pirate",
"collection": "Origins",
"image": "navire_pirate.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 1,
"winner_effect": "... -",
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"effect": "",
"citation": "",
"character": {
"id": -1,
"name": "NoChar"
},
"rarity": {
"id": 8,
"name": "Terrain"
}
},
"type": {
"id": 1,
"name": "Terrain"
}
},
"links": {
"self": "/api/card/1",
"all": "/api/card/",
"image": "/api/image/navire_pirate.webp"
}
},
{
"card": {
"id": 31,
"name": "Garagiste",
"collection": "Origins",
"image": "garagiste_laink.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 31,
"winner_effect": "",
"looser_effect": "",
"special": "",
"effect": "Piochez une carte. COMBO infini +1 en Force.",
"citation": "C'est la rotule d'embrayage de la culasse qu'a pété. On sera dans les 5000 balles, ma bonne dame.",
"character": {
"id": 1,
"name": "Laink"
},
"rarity": {
"id": 1,
"name": "C"
}
},
"type": {
"id": 2,
"name": "Personnage"
}
},
"links": {
"self": "/api/card/31",
"all": "/api/card/",
"image": "/api/image/garagiste_laink.webp"
}
}
]
}
}
API routes for characters
/api/character/:id
{
"character": {
"id": 1,
"name": "Laink"
},
"links": {
"self": "/api/character/1",
"all": "/api/character/",
"cards": "/api/character/1/cards"
}
}
/api/character/
{
"type": "collection",
"length": 2,
"characters": [
{
"character": {
"id": 1,
"name": "Laink"
},
"links": {
"self": "/api/character/1",
"all": "/api/character/",
"cards": "/api/character/1/cards"
}
},
{
"character": {
"id": 2,
"name": "Terracid"
},
"links": {
"self": "/api/character/2",
"all": "/api/character/",
"cards": "/api/character/2/cards"
}
}
]
}
/api/character/:id/cards
{
"character": {
"id": 1,
"name": "Laink"
},
"links": {
"self": "/api/character/1",
"all": "/api/character/",
"cards": "/api/character/1/cards"
},
"cards": {
"type": "collection",
"length": 1,
"cards": [
{
"card": {
"id": 31,
"name": "Garagiste",
"collection": "Origins",
"image": "garagiste_laink.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 31,
"winner_effect": "",
"looser_effect": "",
"special": "",
"effect": "Piochez une carte. COMBO infini +1 en Force.",
"citation": "C'est la rotule d'embrayage de la culasse qu'a pété. On sera dans les 5000 balles, ma bonne dame.",
"character": {
"id": 1,
"name": "Laink"
},
"rarity": {
"id": 1,
"name": "C"
}
},
"type": {
"id": 2,
"name": "Personnage"
}
},
"links": {
"self": "/api/card/31",
"all": "/api/card/",
"image": "/api/image/garagiste_laink.webp"
}
}
]
}
}
API routes for rarities
/api/rarity/:id
{
"rarity": {
"id": 1,
"name": "C"
},
"links": {
"self": "/api/rarity/1",
"all": "/api/rarity/",
"cards": "/api/rarity/1/cards"
}
}
/api/rarity/
{
"type": "collection",
"length": 2,
"rarities": [
{
"rarity": {
"id": 1,
"name": "C"
},
"links": {
"self": "/api/rarity/1",
"all": "/api/rarity/",
"cards": "/api/rarity/1/cards"
}
},
{
"rarity": {
"id": 2,
"name": "UC"
},
"links": {
"self": "/api/rarity/2",
"all": "/api/rarity/",
"cards": "/api/rarity/2/cards"
}
}
]
}
/api/rarity/:id/cards
{
"rarity": {
"id": 1,
"name": "C"
},
"links": {
"self": "/api/rarity/1",
"all": "/api/rarity/",
"cards": "/api/rarity/1/cards"
},
"cards": {
"type": "collection",
"length": 1,
"cards": [
{
"card": {
"id": 31,
"name": "Garagiste",
"collection": "Origins",
"image": "garagiste_laink.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 31,
"winner_effect": "",
"looser_effect": "",
"special": "",
"effect": "Piochez une carte. COMBO infini +1 en Force.",
"citation": "C'est la rotule d'embrayage de la culasse qu'a pété. On sera dans les 5000 balles, ma bonne dame.",
"character": {
"id": 1,
"name": "Laink"
},
"rarity": {
"id": 1,
"name": "C"
}
},
"type": {
"id": 2,
"name": "Personnage"
}
},
"links": {
"self": "/api/card/31",
"all": "/api/card/",
"image": "/api/image/garagiste_laink.webp"
}
}
]
}
}
API routes for types
/api/type/:id
{
"type": {
"id": 1,
"name": "Terrain"
},
"links": {
"self": "/api/type/1",
"all": "/api/type/",
"cards": "/api/type/1/cards"
}
}
/api/type/
{
"type": "collection",
"length": 2,
"types": [
{
"type": {
"id": 1,
"name": "Terrain"
},
"links": {
"self": "/api/type/1",
"all": "/api/type/",
"cards": "/api/type/1/cards"
}
},
{
"type": {
"id": 2,
"name": "Personnage"
},
"links": {
"self": "/api/type/2",
"all": "/api/type/",
"cards": "/api/type/2/cards"
}
}
]
}
/api/type/:id/cards
{
"type": {
"id": 1,
"name": "Terrain"
},
"links": {
"self": "/api/type/1",
"all": "/api/type/",
"cards": "/api/type/1/cards"
},
"cards": {
"type": "collection",
"length": 1,
"cards": [
{
"card": {
"id": 1,
"name": "Navire Pirate",
"collection": "Origins",
"image": "navire_pirate.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 1,
"winner_effect": "... -",
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"effect": "",
"citation": "",
"character": {
"id": -1,
"name": "NoChar"
},
"rarity": {
"id": 8,
"name": "Terrain"
}
},
"type": {
"id": 1,
"name": "Terrain"
}
},
"links": {
"self": "/api/card/1",
"all": "/api/card/",
"image": "/api/image/navire_pirate.webp"
}
}
]
}
}
API routes for cards
/api/card/:id
{
"card": {
"id": 1,
"name": "Navire Pirate",
"collection": "Origins",
"image": "navire_pirate.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 1,
"winner_effect": "... -",
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"effect": "",
"citation": "",
"character": {
"id": -1,
"name": "NoChar"
},
"rarity": {
"id": 8,
"name": "Terrain"
}
},
"type": {
"id": 1,
"name": "Terrain"
}
},
"links": {
"self": "/api/card/1",
"all": "/api/card/",
"image": "/api/image/navire_pirate.webp"
}
}
/api/card/
{
"type": "collection",
"length": 2,
"cards": [
{
"card": {
"id": 1,
"name": "Navire Pirate",
"collection": "Origins",
"image": "navire_pirate.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 1,
"winner_effect": "... -",
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"effect": "",
"citation": "",
"character": {
"id": -1,
"name": "NoChar"
},
"rarity": {
"id": 8,
"name": "Terrain"
}
},
"type": {
"id": 1,
"name": "Terrain"
}
},
"links": {
"self": "/api/card/1",
"all": "/api/card/",
"image": "/api/image/navire_pirate.webp"
}
},
{
"card": {
"id": 31,
"name": "Garagiste",
"collection": "Origins",
"image": "garagiste_laink.webp",
"artist": {
"id": 1,
"name": "Léonard Lam"
},
"description": {
"id": 31,
"winner_effect": "",
"looser_effect": "",
"special": "",
"effect": "Piochez une carte. COMBO infini +1 en Force.",
"citation": "C'est la rotule d'embrayage de la culasse qu'a pété. On sera dans les 5000 balles, ma bonne dame.",
"character": {
"id": 1,
"name": "Laink"
},
"rarity": {
"id": 1,
"name": "C"
}
},
"type": {
"id": 2,
"name": "Personnage"
}
},
"links": {
"self": "/api/card/31",
"all": "/api/card/",
"image": "/api/image/garagiste_laink.webp"
}
}
]
}
API route for images
/api/image/:imageName
Returns the image file.
No matter the request (except auth), you must put a header in your request with your auth token. This token can be obtained by login in before any request :
POST : /auth/authenticate
{
"email": "[email protected]",
"password": "yourpassword"
}
{
"code": 200,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lIjoiRnJpIEp1bCAyMSAyMDIzIDE2OjAwOjE1IEdNVCswMDAwIChDb29yZGluYXRlZCBVbml2ZXJzYWwgVGltZSkiLCJ1c2VyIjoidGltZXV1aEBnbWFpbC5jb20iLCJuYW1lIjoidGltb3Row6llIiwibGFzdG5hbWUiOiJicmluZGVqb25jIiwiaWF0IjoxNjg5OTU1MjE1LCJleHAiOjE2ODk5NTg4MTV9.AsZSag3tLAcpRGLFWYG0l2GwHEHTMwB92s5h9eg2V84"
}
When you have the token, you must put it in a defined header in each request. This header name is defined in .env file mentioned above. For my exemple, it will be wankul_api_test.
In your request, put the header :
Key | Value |
---|---|
wankul_api_test | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0aW1lIjoiRnJpIEp1bCAyMSAyMDIzIDE2OjAwOjE1IEdNVCswMDAwIChDb29yZGluYXRlZCBVbml2ZXJzYWwgVGltZSkiLCJ1c2VyIjoidGltZXV1aEBnbWFpbC5jb20iLCJuYW1lIjoidGltb3Row6llIiwibGFzdG5hbWUiOiJicmluZGVqb25jIiwiaWF0IjoxNjg5OTU1MjE1LCJleHAiOjE2ODk5NTg4MTV9.AsZSag3tLAcpRGLFWYG0l2GwHEHTMwB92s5h9eg2V84 |
If you don't put this header, the response will be of this type :
{
"code": 500,
"error": "RequestError : you must provide a token !"
}
Backend CRUD routes for artists
POST : /artists/new
Key | Value |
---|---|
id | 1 |
name | Ben Giletti |
{
"code": 200,
"artist": {
"id": 1,
"name": "Léonard Lam"
}
}
GET : /artists/get
{
"id": 1
}
{
"code": 200,
"artist": {
"id": 1,
"name": "Léonard Lam"
}
}
PUT : /artists/update
{
"id": 1,
"name": "Léonard Lam"
}
{
"code": 200,
"newArtist": {
"id": 1,
"name": "Léonard Lam"
}
}
DELETE : /artists/delete
{
"id": 1
}
{
"code": 200,
"deletedArtist": {
"id": 1,
"name": "Léonard Lam"
}
}
Backend CRUD routes for cards
POST : /cards/new
Key | Value |
---|---|
artist_id | 1 |
collection | Origins |
description_id | 1 |
id | 1 |
image | navire_pirate.webp |
name | Navire Pirate |
type_id | 1 |
{
"code": 200,
"card": {
"artist_id": 1,
"collection": "Origins",
"description_id": 1,
"id": 1,
"image": "navire_pirate.webp",
"name": "Navire Pirate",
"type_id": 1
}
}
GET : /cards/get
{
"id": 1
}
{
"code": 200,
"card": {
"artist_id": 1,
"collection": "origins",
"description_id": 1,
"id": 1,
"image": "navire_pirate.webp",
"name": "Navire Pirate",
"type_id": 1
}
}
PUT : /cards/update
{
"id": 1,
"name": "Navire Pirate",
"collection": "origins",
"description_id": 1,
"type_id": 1,
"artist_id": 1,
"image": "navire_pirate.png"
}
{
"code": 200,
"newCard": {
"id": 1,
"name": "Navire Pirate",
"collection": "origins",
"description_id": 1,
"type_id": 1,
"artist_id": 1,
"image": "navire_pirate.png"
}
}
DELETE : /cards/delete
{
"id": 1
}
{
"code": 200,
"deletedCard": {
"id": 1,
"name": "Navire Pirate",
"collection": "origins",
"description_id": 1,
"type_id": 1,
"artist_id": 1,
"image": "navire_pirate.png"
}
}
Backend CRUD routes for characters
POST : /characters/new
Key | Value |
---|---|
id | 1 |
name | Navire Pirate |
{
"code": 200,
"character": {
"id": 1,
"name": "Laink"
}
}
GET : /characters/get
{
"id": 1
}
{
"code": 200,
"character": {
"id": 1,
"name": "Laink"
}
}
PUT : /characters/update
{
"character": {
"id": 1,
"name": "Laink"
}
}
{
"code": 200,
"newCharacter": {
"id": 1,
"name": "Laink"
}
}
DELETE : /characters/delete
{
"id": 1
}
{
"code": 200,
"deletedCharacter": {
"id": 1,
"name": "Laink"
}
}
Backend CRUD routes for descriptions
POST : /descriptions/new
Key | Value |
---|---|
character_id | -1 |
citation | |
effect | |
id | 1 |
looser_effect | ... défausse les 5 cartes du dessus du deck. |
rarity_id | 8 |
special | Quand ce Terrain entre en jeu, chaque joueur défausse une carte. |
winner_effect | ... - |
{
"code": 200,
"description": {
"character_id": -1,
"citation": "",
"effect": "",
"id": 1,
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"rarity_id": 8,
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"winner_effect": "... -"
}
}
GET : /descriptions/get
{
"id": 1
}
{
"code": 200,
"description": {
"character_id": -1,
"citation": "",
"effect": "",
"id": 1,
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"rarity_id": 8,
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"winner_effect": "... -"
}
}
PUT : /descriptions/update
{
"character_id": -1,
"citation": "",
"effect": "",
"id": 1,
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"rarity_id": 8,
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"winner_effect": "... -"
}
{
"code": 200,
"newDescription": {
"character_id": -1,
"citation": "",
"effect": "",
"id": 1,
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"rarity_id": 8,
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"winner_effect": "... -"
}
}
DELETE : /descriptions/delete
{
"id": 1
}
{
"code": 200,
"deletedDescription": {
"character_id": -1,
"citation": "",
"effect": "",
"id": 1,
"looser_effect": "... défausse les 5 cartes du dessus du deck.",
"rarity_id": 8,
"special": "Quand ce Terrain entre en jeu, chaque joueur défausse une carte.",
"winner_effect": "... -"
}
}
Backend CRUD routes for rarities
POST : /rarities/new
Key | Value |
---|---|
id | 1 |
name | C |
{
"code": 200,
"rarity": {
"id": 1,
"name": "C"
}
}
GET : /rarities/get
{
"id": 1
}
{
"code": 200,
"rarity": {
"id": 1,
"name": "C"
}
}
PUT : /rarities/update
{
"id": 1,
"name": "C"
}
{
"code": 200,
"newRarity": {
"id": 1,
"name": "C"
}
}
DELETE : /rarities/delete
{
"id": 1
}
{
"code": 200,
"deletedRarity": {
"id": 1,
"name": "C"
}
}
Backend CRUD routes for types
POST : /types/new
Key | Value |
---|---|
id | 1 |
name | Terrain |
{
"code": 200,
"type": {
"id": 1,
"name": "Terrain"
}
}
GET : /types/get
{
"id": 1
}
{
"code": 200,
"type": {
"id": 1,
"name": "Terrain"
}
}
PUT : /types/update
{
"id": 1,
"name": "Terrain"
}
{
"code": 200,
"newType": {
"id": 1,
"name": "Terrain"
}
}
DELETE : /types/delete
{
"id": 1
}
{
"code": 200,
"deletedType": {
"id": 1,
"name": "Terrain"
}
}
Backend CRUD routes for users
POST : /users/new
Key | Value |
---|---|
[email protected] | |
name | john |
lastname | doe |
password | Terrain |
{
"code": 200,
"user": {
"email": "[email protected]",
"lastname": "john",
"name": "doe"
}
}
GET : /users/get
{
"email": "[email protected]"
}
{
"code": 200,
"user": {
"email": "[email protected]",
"lastname": "john",
"name": "doe"
}
}
PUT : /users/update
{
"email": "[email protected]",
"lastname": "john",
"name": "doe",
"password": "$2y$10$wpf6.oOtNUgpFGs67TY.huV4IvOBk6RyJfnUmFJZNwyJXWQ/MJNP2"
}
{
"code": 200,
"newUser": {
"email": "[email protected]",
"lastname": "john",
"name": "doe",
"password": "$2y$10$wpf6.oOtNUgpFGs67TY.huV4IvOBk6RyJfnUmFJZNwyJXWQ/MJNP2"
}
}
DELETE : /users/delete
{
"email": "[email protected]"
}
{
"code": 200,
"deletedUser": {
"email": "[email protected]",
"lastname": "john",
"name": "doe",
"password": "$2y$10$wpf6.oOtNUgpFGs67TY.huV4IvOBk6RyJfnUmFJZNwyJXWQ/MJNP2"
}
}
This project is my first backend Javascript project. I learned a lot while trying to figure out how to do what I wanted.
Here are the technologies I used to build this API / backend app :
Once again, all this project is based on Wankul cards, created by Wankil Studio and accessible on this link.
This is not in association in any way with the creators of Wankul cards.
As for the author of this project, credits goes to Timeuh.