Giter Club home page Giter Club logo

dotnet-fake-json-server's Introduction

Fake JSON Server

NuGet

Build server Platform Build status
Travis Linux / macOS Build Status
CircleCI Windows CircleCI

Fake JSON Server is a Fake REST API that can be used as a Back End for prototyping or as a template for a CRUD Back End. Fake JSON Server also has an experimental GraphQL query and mutation support.

  • No need to define types for resources, uses dynamic typing
  • No need to define routes, routes are handled dynamically
  • No database, data is stored to a single JSON file
  • No setup required, just start the server and API is ready to be used with any data

Why to use this?

  1. API is built following the best practices and can be used as a reference when building your own API
  2. Contains all common features used with well functioning APIs (see features listed below)
  3. Can be run on Windows, Linux and macOS without any installation or prerequisites from executable or with Docker

Docs website

https://ttu.github.io/dotnet-fake-json-server/


Features

  • Supported HTTP methods #
    • All methods for CRUD operations (GET, PUT, POST, PATCH, DELETE)
    • Methods for fetching resource information (HEAD, OPTIONS)
  • Async versions of update operations with long running operations and queues #
  • REST API follows best practices from multiple guides
    • Uses correct Status Codes, Headers, etc.
    • As all guides have slightly different recommendations, this compilation is based on our opinions
  • Paging, filtering, selecting, text search etc. #
  • Token, Basic and API key Authentication #
  • WebSocket update notifications #
  • Simulate delay and errors for requests #
  • Static files #
  • Swagger #
  • CORS #
  • Content Negotiation (output formats JSON, CSV and XML) #
  • Caching and avoiding mid-air collisions with ETag #
  • Configurable custom response transformation #
  • Experimental GraphQL query and mutation support #

Developed with

Table of contents

Click to here to see contents

Get started

.NET CLI

# Get source code from GitHub
$ git clone https://github.com/ttu/dotnet-fake-json-server.git

$ cd dotnet-fake-json-server/FakeServer
$ dotnet run

Start server with defined data-file and url (optional arguments)

# Optional arguments:
#   --file <FILE>    Data store's JSON file (default datastore.json)
#   --urls <URL>     Server url (default http://localhost:57602)      
#   --serve <PATH>   Serve static files (default wwwroot)
#   --version        Prints the version of the app

$ dotnet run --file data.json --urls http://localhost:57602

Dotnet global tool

Server can be installed as a dotnet global tool. Settings files are then located at %USERPROFILE%\.dotnet\tools (Windows) and $HOME/.dotnet/tools (Linux/macOS). By default data stores's JSON file will be created to execution directory.

# install as a global tool
$ dotnet tool install --global FakeServer

# Example: Start server
$ fake-server --file data.json --urls http://localhost:57602

# Update to the newest version
$ dotnet tool update --global FakeServer

Docker

If you don't have .NET installed, you can run the server with Docker.

# Get source code from GitHub
$ git clone https://github.com/ttu/dotnet-fake-json-server.git

$ cd dotnet-fake-json-server
$ docker build -t fakeapi .

# Run in foreground
$ docker run -it -p 57602:57602 --name fakeapi fakeapi

# Run in detached mode (run in background)
$ docker run -it -d -p 57602:57602 --name fakeapi fakeapi

# Start stopped container (remove -a to run in background)
$ docker start -a fakeapi

Copy JSON-file to/from container. Filename is datastore.json

# Copy file from host to container
$ docker cp datastore.json fakeapi:/app/datastore.json

# Copy file from container to host
$ docker cp fakeapi:/app/datastore.json datastore.json

Self-contained application

The self-contained application archive contains Fake JSON Server, .NET runtime and all required third-party dependencies. No installation or prerequisites are needed.

  1. Go to Latest Release
  2. Download correct archive matching your OS
  3. Extract files and execute

E.g. download and execute version 0.11.0 for macOS

$ mkdir FakeServer && cd FakeServer
$ wget https://github.com/ttu/dotnet-fake-json-server/releases/download/0.11.0/fakeserver-osx-x64.tar.gz
$ tar -zxvf fakeserver-osx-x64.tar.gz
$ chmod +x FakeServer
$ ./FakeServer

Serve static files

Fake Server can serve static files. Location of files can be absolute or relative to the current location.

$ dotnet run -s/--serve [fullpath/relative path]
# e.g.
$ dotnet run -s build

# Use Fake Server as a global tool
$ fake-server -s/--serve [fullpath/relative path]]
# e.g.
$ fake-server --serve c:\temp\react_app\build
$ fake-server --serve /home/user/app/dist
$ fake-server --serve ./build

When user defines static files, it is assumed that user is serving a single page app and then REST API is not working. If API is needed, start other instance of Fake Server.

Examples

Quick example

# List collections (should be empty, if data.json didn't exist before)
$ curl http://localhost:57602/api

# Insert new user
$ curl -H "Content-type: application/json" -X POST -d '{ "name": "Phil", "age": 20, "location": "NY" }' http://localhost:57602/api/users/

# Insert another user
$ curl -H "Content-type: application/json" -X POST -d '{ "name": "James", "age": 40, "location": "SF" }' http://localhost:57602/api/users/

# List users
$ curl http://localhost:57602/api/users

# List users from NY
$ curl http://localhost:57602/api/users?location=NY

# Get User with Id 1
$ curl http://localhost:57602/api/users/1

# ...

# Add users to data.json manually

# Get all users
$ curl http://localhost:57602/api/users/
# ...

# Or open url http://localhost:57602/swagger/ with browser and use Swagger

Example project

Redux TodoMVC example modified to use Fake JSON Server as a Back End.

Example queries

Example queries are in Insomnia workspace format in FakeServer_Insomnia_Workspace.json.

Features

Authentication

Fake REST API supports Token and Basic authentication and API keys.

Authentication can be disabled from appsettings.json by setting Enabled to false. AuthenticationType options are token, basic and apikey.

Add allowed usernames/passwords to Users-array. Add optional API key to ApiKey-property.

"Authentication": {
  "Enabled": true,
  "AuthenticationType": "token",
  "Users": [
      { "Username": "admin", "Password": "root" }
  ],
  "ApiKey": "abcd1234"
}

Token authentication

API has a token provider middleware which provides an endpoint for token generation /token. Endpoint supports 'content-type: multipart/form-data and content-type: application/json. Username and password must be in username and password fields.

Get token:

# content-type: multipart/form-data
$ curl -X POST -H 'content-type: multipart/form-data' -F username=admin -F password=root http://localhost:57602/token

# content-type: application/json
$ curl -X POST -H 'content-type: application/json' -d '{ "username": "admin", "password": "root" }' http://localhost:57602/token

Token can be fetch also using Client Credentials grant type (see example from Insomnia workspace):

$ curl -X POST -d "grant_type=client_credentials&client_id=admin&client_secret=root" http://localhost:57602/token

Add token to Authorization header:

$ curl -H 'Authorization: Bearer [TOKEN]' http://localhost:57602/api

Token authentication supports logout functionality. By design, tokens do not support token invalidation and logout is implemented by blacklisting tokens.

$ curl -X POST -d '' -H 'Authorization: Bearer [TOKEN]' http://localhost:57602/logout

The implementation is quite similiar to SimpleTokenProvider and more info on that can be found from GitHub and StormPath's blog post.

Basic authentication

NOTE: It is not recommended to use Basic Authentication in production as base64 is a reversible encoding

Add base64 encoded username:password to authorization header e.g. 'Authorization: Basic YWRtaW46cm9vdA=='.

$ curl -u admin:root http://localhost:57602/api
# -u argument creates Authorization header with encoded username and password
$ curl -H 'Authorization: Basic YWRtaW46cm9vdA==' http://localhost:57602/api

API key authentication

Add key set to Authentication settings to X-API-KEY header e.g. X-API-KEY: abcd1234'.

$ curl -H 'X-API-KEY: abcd1234' http://localhost:57602/api

WebSockets

API will send the latest update's method (POST, PUT, PATCH, DELETE), path, collection and optional item id with WebSocket.

{ "method": "PATCH", "path": "/api/users/2", "collection": "users", "itemId": 2 }

wwwroot\index.html has a WebSocket example.

CORS

CORS is enabled and it allows everything.

Static files

GET /

Returns static files from wwwroot or defined location. Default file is index.html.

Check how to serve static files from defined location.

Swagger

Swagger is configured to endpoint /swagger and Swagger UI opens when project is started from IDE.

Caching and avoiding mid-air collisions with ETag

Caching can be disabled from appsettings.json by setting ETag.Enabled to false.

"Caching": {
  "ETag": { 
    "Enabled": true 
  }
}

If caching is enabled, ETag is added to response headers.

$ curl -v 'http://localhost:57602/api/users?age=40'
200 OK

Headers:
ETag: "5yZCXmjhk5ozJyTK4-OJkkd_X18"

Caching of unchanged resources

If a request contains the If-None-Match header, the header's value is compared to the response's body and if the value matches to the body's checksum then 304 Not Modified is returned.

$ curl -H "If-None-Match: \"5yZCXmjhk5ozJyTK4-OJkkd_X18\"" 'http://localhost:57602/api/users?age=40'

Avoiding mid-air collisions

If the PUT request contains the If-Match header, the header's value is compared to the item to be updated. If the value matches to the item's checksum then items is updated, else 412 Precondition Failed is returned.

Content negotiaton

Client can determine what type of representation is desired with Accept header. By default data is returned in JSON (text/json, application/json).

Supported types are JSON, CSV and XML.

text/json
application/json
text/csv
text/xml
application/xml

Get all users in CSV

$ curl -H "Accept: text/csv" http://localhost:57603/api/users

If content types is not supported 406 Not Acceptable is returned.

Simulate Delay and Random Errors

Delay and errors can be configured from appsettings.json.

Delay can be simulated by setting Simulate.Delay.Enabled to true. The inbound request is delayed. The length of the delay is randomly chosen between MinMsand MaxMs. Delay can be configured for only certain HTTP Methods, e.g. only POST updates have delay and all GET requests are handled normally.

"Simulate": {
    "Delay": {
      "Enabled": true,
      "Methods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ],
      "MinMs": 2000,
      "MaxMs": 5000
    }
}

Random errors can be simulated by setting Simulate.Error.Enabled to true. Error is thrown if set Probability is greater or equal to randomly chosen value between 1 and 100. Error can be configured for only certain HTTP Methods.

"Simulate": {
    "Error": {
      "Enabled": true,
      "Methods": [ "POST", "PUT", "PATCH", "DELETE" ],
      "Probability": 50
    }
}

Error simulation is always skipped for Swagger, WebSocket (ws) and for any html file.

Custom Response Transformation

Fake Server has a custom response middleware to transform reponse body with C#-scripts.

Multiple scripts can be configured and if path matches multiple scipts, last match will be used.

"CustomResponse": {
    "Enabled": true,
    "Scripts": [
        {
            "Script": "return new { Data = _Body, Success = _Context.Response.StatusCode == 200 };",
            "Methods": [ "GET" ],
            "Paths": [ "api" ],
            "Usings": [ "System", "Microsoft.AspNetCore.Http" ],
            "References": [ "Microsoft.AspNetCore" ]
        },
        {
            "Script": "return new { Data = \"illegal operation\" };",
            "Methods": [ "GET" ],
            "Paths": [ "api/users" ],
            "Usings": [ "System", "Microsoft.AspNetCore.Http" ],
            "References": [ "Microsoft.AspNetCore" ]
      }
    ]
}

C# code is executed as a csscript and it has some special reacy processed objects.

// HttpContext
public HttpContext _Context;
// Collection id parsed from the Request path
public string _CollectionId;
// Original Response Body encoded to string
public string _Body;
// Request Http Method
public string _Method;

Example script creates new anonymous object

return new { Data = _Body, Success = _Context.Response.StatusCode == 200 };

Previous script will have a response body:

{
  "Data": [
    { "id": 1, "name": "James" ...},
    { "id": 2, "name": "Phil", ... },
    ...
  ],
  "Success": true
}

If response data requires so dynamically named properties, e.g. users in the example, then response requires more complex processing.

{
  "Data": {
    "users": [
      { "id": 1, "name": "James" ...},
      { "id": 2, "name": "Phil", ... },
      ...
    ]
  },
  "Success": true
}

C#-code for the processing would be following:

var data = new ExpandoObject();
var dataItems = data as IDictionary<string, object>;
dataItems.Add(_CollectionId, _Body);

var body = new ExpandoObject();
var items = body as IDictionary<string, object>;
items.Add("Data", data);
items.Add("Success", _Context.Response.StatusCode == 200);
return body;

Script also would need System.Collections.Generic and System.Dynamic as imports.

{
    "Script": "var data = new ExpandoObject();var dataItems = data as IDictionary<string, object>;dataItems.Add(_CollectionId, _Body);var body = new ExpandoObject();var items = body as IDictionary<string, object>;items.Add(\"Data\", data);items.Add(\"Success\", _Context.Response.StatusCode == 200);return body;",
    "Methods": [ "GET" ],
    "Paths": [ "api" ],
    "Usings": [ "System", "System.Dynamic", "System.Collections.Generic", "Microsoft.AspNetCore.Http" ],
    "References": [ "Microsoft.AspNetCore" ]
}

Logging

Fake JSON Server writes a log file to the application base path (execution folder).

Console logging can be enabled from appsettings.json by adding a new item to Serilog.WriteTo-array.

"Serilog": {
  "WriteTo": [
    { "Name": "File" },
    { "Name": "Console" }
  ]
}

Routes, functionalities and examples

GET      /
POST     /token
POST     /logout
POST     /admin/reload

GET      /api
HEAD     /api
GET      /api/{collection/object}
HEAD     /api/{collection/object}
POST     /api/{collection}
GET      /api/{collection}/{id}
HEAD     /api/{collection}/{id}
PUT      /api/{collection}/{id}
PATCH    /api/{collection}/{id}
DELETE   /api/{collection}/{id}
PUT      /api/{object}
PATCH    /api/{object}
DELETE   /api/{object}
OPTIONS  /api/*

GET      /async/queue/{id}
DELETE   /async/queue/{id}
POST     /async/{collection}
PUT      /async/{collection}/{id}
PATCH    /async/{collection}/{id}
DELETE   /async/{collection}/{id}
OPTIONS  /async/*

POST     /graphql

Collections and objects

Fake JSON Server is designed for prototyping, so by default it supports only resources in a collection.

If the JSON-file has a single object on a root level, then the route from that property is handled like a single object.

{
  "collection": [],
  "object": {}
}

Routes

Dynamic routes are defined by the name of item's collection and id: api/{collection}/{id}. All examples below use users as a collection name.

If /api or /async are needed to change to something different, change ApiRoute or AsyncRoute from Config.cs.

public class Config
{
    public const string ApiRoute = "api";
    public const string AsyncRoute = "async";
    public const string GraphQLRoute = "graphql";
    public const string TokenRoute = "token";
    public const string TokenLogoutRoute = "logout";
}

For example, if api-prefix is not wanted in the route, then remove api from ApiRoute.

public const string ApiRoute = "";
# Query with default route
$ curl 'http://localhost:57602/api/users?skip=5&take=20'
# Query with updated route
$ curl 'http://localhost:57602/users?skip=5&take=20'

Identifiers

id is used as the identifier field. By default Id field's type is integer. POST will always use integer as id field's type.

"users":[
  { "id": 1 }
],
"sensors": [
  { "id": "E:52:F7:B3:65:CC" }
]

If string is used as the identifiers type, then items must be inserted with PUT and UpsertOnPut must be set to true from appsettings.json.

HTTP return codes

Method return codes are specified in REST API Tutorial.

Asynchoronous operations follow the REST CookBook guide. Updates will return 202 with location header to queue item. Queue will return 200 while operation is processing and 303 when job is ready with location header to changed or new item.

Data Store Id-field name

Name of the Id-field used by Data Store can be configure from appsettings.json. Default name for the id-field is id.

"DataStore": {
  "IdField": "id"
}

Eager data reload

By default Data Store updates its internal data on every request by reading the data from the JSON file.

EagerDataReload can be configured from appsettings.json.

"DataStore": {
  "EagerDataReload": true
}

For performance reasons EagerDataReload can be changed to false. Then the data is reloaded from the file only when Data Store is initialized and when the data is updated.

If EagerDataReload is false and JSON file is updated manually, reload endpoint must be called if new data will be queried before any updates.

Reload

Reload endpoint can be used to reload JSON data from the file to Data Store. Endpoint is in Admin controller, so it is usable also with Swagger.

$ curl -X POST http://localhost:57602/admin/reload --data ""

Endpoints

JSON data used in examples

Data used in example requests, unless otherwise stated:

{
  "users": [
    { "id": 1, "name": "Phil", "age": 40, "location": "NY" },
    { "id": 2, "name": "Larry", "age": 37, "location": "London" },
    { "id": 3, "name": "Thomas", "age": 40, "location": "London" }
  ],
  "movies": [],
  "configuration": { "ip": "192.168.0.1" }
}

Example JSON generation guide for data used in unit tests CreateJSON.md.

List collections (GET)

> GET /api

200 OK : List of collections

Get all collections.

$ curl http://localhost:57602/api
[ "users", "movies", "configuration" ]

Query items (GET)

> GET /api/{collection}

200 OK          : Collection is found
400 Bad Request : Invalid query parameters
404 Not Found   : Collection is not found

By default the request returns results in an array. Headers have the collection's total item count (X-Total-Count) and pagination links (Link).

$ curl http://localhost:57602/api/users
[
  { "id": 1, "name": "Phil", "age": 40, "location": "NY" },
  { "id": 2, "name": "Larry", "age": 37, "location": "London" },
  { "id": 3, "name": "Thomas", "age": 40, "location": "London" },
  ...
]

Headers:
X-Total-Count=20
Link=
<http://localhost:57602/api/users?offset=15&limit=5>; rel="next",
<http://localhost:57602/api/users?offset=15&limit=5>; rel="last",
<http://localhost:57602/api/users?offset=0&limit=5>; rel="first",
<http://localhost:57602/api/users?offset=5&limit=5>; rel="prev"

The return value can also be a JSON object. Set UseResultObject to true from appsettings.json.

"Api": {
  "UseResultObject": true
}

JSON object has items in results array in result field, link object has the pagination info, skip, take and total count fields.

{
  "results": [],
  "link": {
    "Prev": "http://localhost:57602/api/users?offset=5&limit=5",
    "Next": "http://localhost:57602/api/users?offset=15&limit=5",
    "First": "http://localhost:57602/api/users?offset=0&limit=5",
    "Last": "http://localhost:57602/api/users?offset=15&limit=5"
  },
  "offset": 10,
  "limit": 5,
  "count": 20
}

Single object doesn't support result object. If the endpoint is a single object, only item object is returned.

$ curl http://localhost:57602/api/configuration
{ "ip": "192.168.0.1" }
Slice

Slicing can be defined with skip/take, offset/limit or page/per_page parameters. By default request returns the first 512 items.

Example request returns items from 11 to 20.

# skip and take
$ curl 'http://localhost:57602/api/users?skip=10&take=10'
# offset and limit
$ curl 'http://localhost:57602/api/users?offset=10&limit=10'
# page and per_page
$ curl 'http://localhost:57602/api/users?page=2&per_page=10'
Pagination headers

Link items are optional, so e.g. if requested items are starting from index 0, then the prev and first page link won't be added to the Link header.

Headers follow GitHub Developer guide.

Sort
> GET api/{collection}?sort=[+/-]field,[+/-]otherField

Sort contains comma-spearetd list of fields defining the sort. Sort direction can be specified with + (ascending) or - (descending, default) prefix.

Get all users sorted by location (descending) and then by age (ascending).

$ curl 'http://localhost:57602/api/users?sort=location,+age'
[
  { "id": 2, "name": "Larry", "age": 37, "location": "London" },
  { "id": 3, "name": "Thomas", "age": 40, "location": "London" },
  { "id": 1, "name": "Phil", "age": 40, "location": "NY" }
]
Filter
> GET api/{collection}?field=value&otherField=value

Get all users whose age equals to 40.

$ curl 'http://localhost:57602/api/users?age=40'
[ 
 { "id": 1, "name": "Phil", "age": 40, "location": "NY" },
 { "id": 3, "name": "Thomas", "age": 40, "location": "London" }
]
Filter operators

Query filter can include operators. Operator identifier is added to the end of the field.

> GET api/{collection}?field{operator}=value

=     : Equal to
_ne=  : Not equal
_lt=  : Less than
_gt=  : Greater than
_lte= : Less than or equal to
_gte= : Greater than or equal to

Query users with age less than 40.

$ curl http://localhost:57602/api/users?age_lt=40
[ 
  { "id": 2, "name": "Larry", "age": 37, "location": "London" }
]
Filter with child properties

Query can have a path to child properties. Property names are separated by periods.

> GET api/{collection}?parent.child.grandchild.field=value

Example JSON:

[
  {
    "companyName": "ACME",
    "employees": [ 
      { "id": 1, "name": "Thomas", "address": { "city": "London" } }
    ]
  },
  {
    "companyName": "Box Company",
    "employees": [ 
      { "id": 1, "name": "Phil", "address": { "city": "NY" } }
    ]
  }
]

Get all companies which has employees with London in address.city.

$ curl http://localhost:57602/api/companies?employees.address.city=London

Query will return ACME from the example JSON.

[
  {
    "companyName": "ACME",
    "employees": [ 
      { "id": 1, "name": "Thomas", "address": { "city": "London" } }
    ]
  }
]
Full-text search

Full-text search can be performed with the q-parameter followed by search text. Search is not case sensitive.

> GET api/{collection}?q={text}

Get all users that contain text London in the value of any of it's properties.

$ curl http://localhost:57602/api/users?q=london
Select fields

Choose which fields to include in the results. Field names are separated by comma.

> GET api/{collection}?fields={fields}

Select age and name from users.

$ curl http://localhost:57602/api/users?fields=age,name
[ 
  { "name": "Phil", "age": 40 },
  { "name": "Larry", "age": 37 }
]

Get item with id (GET)

> GET /api/{collection}/{id}

200 OK          : Item is found
400 Bad Request : Collection is not found
404 Not Found   : Item is not found

Get user with id 1.

$ curl http://localhost:57602/api/users/1
{ "id": 1, "name": "Phil", "age": 40, "location": "NY" }
Get nested items
> GET /api/{collection}/{id}/{restOfThePath}

200 OK          : Nested item is found
400 Bad Request : Parent item is not found
404 Not Found   : Nested item is not found

It is possible to request only child objects instead of full item. Path to nested item can contain id field integers and property names.

[
  {
    "id": 0,
    "companyName": "ACME",
    "employees": [ 
      { "id": 1, "name": "Thomas", "address": { "city": "London" } }
    ]
  }
]

Example query will return address object from the employee with id 1 from the company with id 0.

$ curl http://localhost:57602/api/company/0/employees/1/address
{ "address": { "city": "London" } }

Add item (POST)

> POST /api/{collection}

201 Created     : New item is created
400 Bad Request : New item is null
409 Conflict    : Collection is an object

Add { "name": "Phil", "age": 40, "location": "NY" } to users.

$ curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "name": "Phil", "age": 40, "location": "NY" }' http://localhost:57602/api/users/

Response has new item's id and a Location header that contains the path to the new item.

{ "id": 6 }

Headers:
Location=http://localhost:57602/api/users/6

If collection is empty and new item has an id-field set, it will be used a first id-value. If id-field is not set, id-value will start from 0.

Replace item (PUT)

> PUT /api/{collection}/{id}

204 No Content  : Item is replaced
400 Bad Request : Item is null
404 Not Found   : Item is not found

Replace user with id 1 with object { "name": "Roger", "age": 28, "location": "SF" }.

$ curl -H "Accept: application/json" -H "Content-type: application/json" -X PUT -d '{ "name": "Roger", "age": 28, "location": "SF" }' http://localhost:57602/api/users/1

Update item (PATCH)

Server supports JSON patch and JSON merge patch.

JSON Patch
> PATCH /api/{collection}/{id}

Content-type: application/json-patch+json

204 No Content             : Item updated
400 Bad Request            : PATCH is empty
404 Not Found              : Item is not found
415 Unsupported Media Type : Content type is not supported

Set age to 41 and work.rating to 3.2 from user with id 1.

$ curl -H "Accept: application/json" -H "Content-type: application/json-patch+json" -X PATCH -d '[{ "op": "replace", "path": "age", "value": 41}, { "op": "replace", "path": "work/rating", "value": 3.2 }]' http://localhost:57602/api/users/1
[
  { "op": "replace", "path": "age", "value": 41}, 
  { "op": "replace", "path": "work/rating", "value": 3.2 }
] 
JSON Merge Patch
> PATCH /api/{collection}/{id}

Content-type: application/json+merge-patch or application/merge-patch+json

204 No Content             : Item updated
400 Bad Request            : PATCH is empty
404 Not Found              : Item is not found
415 Unsupported Media Type : Content type is not supported

Set age to 41 and work.rating to 3.2 from user with id 1.

$ curl -H "Accept: application/json" -H "Content-type: application/json+merge-patch" -X PATCH -d '{ "age": "41", "work": { "rating": 3.2 }}' http://localhost:57602/api/users/1
{
  "age": 41,
  "work": {
    "rating": 3.2
  }
} 

NOTE:

Due to the limitations of the merge patch, if the patch is anything other than an object, the result will always be to replace the entire target with the entire patch. Also, it is not possible to patch part of a target that is not an object, such as to replace just some of the values in an array.

https://tools.ietf.org/html/rfc7396#section-2

Delete item (DELETE)

> DELETE /api/{collection}/{id}

204 No Content  : Item deleted
404 Not Found   : Item is not found

Delete user with id 1.

$ curl -X DELETE http://localhost:57602/api/users/1

OPTIONS method

OPTIONS method will return Allow header with a list of HTTP methods that may be used on the resource.

$ curl -X OPTIONS -v http://localhost:57602/api/
200 OK

Headers:
Allow: GET, POST, OPTIONS

HEAD method

HEAD method can be used to get the metadata and headers without receiving response body.

E.g. get user count without downloading large response body.

$ curl -X HEAD -v http://localhost:57602/api/users
200 OK

Headers:
X-Total-Count: 1249

Async Operations

/async endpoint has long running operation for each update operation.

> POST/PUT/PATCH/DELETE /async/{collection}/{id}

202 Accepted    : New job started
400 Bad Request : Job not started

Headers:
Location=http://{url}:{port}/async/queue/{id}

Update operations will return location to job queue in headers.

Create new item. Curl has a verbose flag (-v). When it is used, curl will print response headers among other information.

$ curl -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{ "name": "Phil", "age": 40, "location": "NY" }' -v http://localhost:57602/async/users/
> GET /async/queue/{id}

200 OK        : Job running
303 See Other : Job ready
404 Not Found : Job not found

Headers:
Location=http://{url}:{port}/api/{collectionId}/{id}

When Job is ready, status code will be redirect See Other. Location header will have modified item's url.

After job is finished, it must be deleted manually

> DELETE /async/queue/{id}

204 No Content : Job deleted
404 Not Found  : Job not found

Job delay

Delay for operations can be set from appsettings.json. With long delay it is easier to simulate long running jobs.

  "Jobs": {
    "DelayMs": 2000
  }

Delay value is milliseconds. Default value is 2000ms.

GraphQL

GraphQL implementation is experimental and supports only basic queries and mutations. At the moment this is a good way to compare simple GraphQL and REST queries. /graphql endpoint accepts requests with application/graphql or application/json content type. If the first, request body is GraphQL query string, whereas if the latter, request body is expected to be a valid JSON with parameter query containing the GraphQL query string.

> POST /graphql

Content-type: application/graphql
Body: [query/mutation]

OR

Content-type: application/json
Body: { "query": "[query/mutation]" }

200 OK              : Query/mutation successful 
400 Bad Request     : Query/mutation contains errors
501 Not Implemented : HTTP method and/or content-type combination not implemented

Alternatively, the /graphql endpoint also supports requests containing a valid GraphQL query as a query query parameter using either a GET or POST request. Queries in the JSON format are not supported as query parameters. Note that in the case of a POST request, the query supplied using the query parameter will take priority over any content in the request body, which will be ignored if the query query parameter is present.

> GET /graphql?query=[query/mutation]

OR

> POST /graphql?query=[query/mutation]

200 OK              : Query/mutation successful 
400 Bad Request     : Query/mutation contains errors
501 Not Implemented : HTTP method and/or content-type combination not implemented

Response is in JSON format. It contains data and errors fields. errors field is not present if there are no errors.

{
  "data": { 
    "users": [ ... ],
    ...
  },
  "errors": [ ... ]
}

Implementation uses graphql-dotnet to parse Abstract Syntax Tree from the query.

Query

Query implementation supports equal filtering with arguments. Query's first field is the name of the collection.

query {
  [collection](filter: value) {
    [field1]
    [field2](filter: value) {
      [field2.1]
    }
    [field3]
  }
}

Implementation accepts queries with operation type, with any query name (which is ignored) and query shorthands.

# Operation type
query {
  users(id: 3) {
    name
    work {
      location
    }
  }
}

# Optional query name
query getUsers {
  users {
    name
    work {
      location
    }
  }
}

# Query shorthand
{
  families {
    familyName
    children(age: 5){
      name
    }
  }
}

Example: get familyName and age of the children from families where id is 1 and namefrom all users.

{
  families(id: 1) {
    familyName
    children {
      age
    }
  }
  users {
    name
  }
}

Execute query with curl:

$ curl -H "Content-type: application/graphql" -X POST -d "{ families(id: 1) { familyName children { age } } users { name } }" http://localhost:57602/graphql

Respose:

{ 
  "data": {
    "families": [ 
      { 
        "familyName": "Day", 
        "children": [ 
          { "age": 14 }, 
          { "age": 18 }, 
          { "age": 9 } 
        ] 
      }
    ],
    "users": [ 
      { "name": "James" }, 
      { "name": "Phil" }, 
      { "name": "Raymond" }, 
      { "name": "Jarvis" } 
    ] 
  }
}

Mutation

Fake JSON Server supports dynamic mutations with the format defined below:

mutation {
  [mutationName](input: {
    [optional id]
    [itemData/patch]
    }) {
      [collection]{
        [fields]
    }
}

Action is decided from the mutation name. Name follows pattern add|update|replace|delete[collection] E.g. deleteUsers will delete an item from the users collection. Input object has an optional id-field and update data object. Return data is defined the same way as in queries.

Add item

add{collection}

Input contains object to be added with the collection's name.

mutation {
  addUsers(input: {
    users: {
      name: James
      work: {
        name: ACME
      }
    }
  }) {
    users {
      id
      name 
    }
  }
}

Execute mutation with curl:

$ curl -H "Content-type: application/graphql" -X POST -d "mutation { addUsers(input: { users: { name: James work: { name: ACME } } }) { users { id name } } }" http://localhost:57602/graphql

Response:

{ 
  "data": {
    "users":{
      "id": 12,
      "name": "James"
    }
  }
}
Update Item

update{collection}

mutation {
  updateUsers(input: {
    id: 2
    patch:{
      name: Timothy
    }
  }) {
    users {
      id
      name 
    }
  }
}

Execute mutation with curl:

$ curl -H "Content-type: application/graphql" -X POST -d "mutation { updateUsers(input: { id: 2 patch:{ name: Timothy } }) { users { id name age }}}" http://localhost:57602/graphql

Response:

{ 
  "data": { 
    "users": { 
      "id": 2, 
      "name": "Timothy", 
      "age": 25 
    } 
  } 
}

NOTE: Update doesn't support updating child arrays

Replace item

replace{collection}

Input must contain id of the item to be replacecd and items full data in object named with collection's name.

mutation {
  replaceUsers(input: {
    id: 5
    users:{
      name: Rick
      age: 44
      workplace: {
       companyName: ACME 
      }
    }
  }) {
    users {
      id
      name
      age
    }
  }
}

Execute mutation with curl:

$ curl -H "Content-type: application/graphql" -X POST -d "mutation { replaceUsers(input: { id: 1 users: { name: Rick age: 44 workplace: { name: ACME } } }) {users {id name age}}}" http://localhost:57602/graphql

Response:

{
  "data": {
    "users": {
      "id": 1,
      "name": "Rick",
      "age": 44
    }
  }
}
Delete item

delete{collection}

Delete requires only the id of item to be deleted. Response will only contain success boolean true/false, so mutation doesn't need any definition for return data.

mutation {
  deleteUsers(input: {
    id: 4
  })
}

Execute mutation with curl:

$ curl -H "Content-type: application/graphql" -X POST -d "mutation { deleteUsers(input: { id: 4 }) }" http://localhost:57602/graphql

Response:

{
  "data": {
    "Result": true
  }
}

Guidelines

API follows best practices and recommendations from these guides:

Other Links

Releases

Releases are marked with Tag and can be found from Releases.

Changelog

Changelog

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

Licensed under the MIT License.

dotnet-fake-json-server's People

Contributors

5wdgjibxs7dee avatar antoniaelek avatar arashjfz avatar aschuhardt avatar blueberryking avatar chekkan avatar erichoog avatar frenzyfunky avatar herariom avatar kashifsoofi avatar kjaek avatar leplaekevin avatar lucarine avatar mat-czernek avatar maymike321 avatar phaniva avatar seleregb avatar sunithapatel avatar ttu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dotnet-fake-json-server's Issues

Add functionality - REST: Sort & direction

Follow guide from Zalando:

sort โ€” comma-separated list of fields to sort. To indicate sorting direction, fields my prefixed with + (ascending) or - (descending, default), e.g. /sales-orders?sort=+id

Sort user by age (descending)

/users?sort=age
/users?sort=-age

Sort user by age (ascending)

/users?sort=+age

HEAD methods don't return ETag header

HEAD method should use same method as GET from DynamicController, so ETagMiddleware has the response body and can add ETag header. Framework will remove the body later.

Fix XML element naming for collections and single items

Regardless of collection naming, elements in XML should be singularized / pluralized.

E.g. for endpoints
/api/family
/api/families

XML should be:

<families>
  <family>
    <id>

Current implementation has e.g. for /api/families

<families>
  <families_0>
    <id>

for /api/family

<family>
  <family_0>
    <id>

Implementation:

var itemName = ObjectHelper.GetCollectionFromPath(context.HttpContext.Request.Path.Value);

var children = itemCollection.Select((i, idx) => ((ExpandoObject)i).Aggregate(new XElement($"{name}_{idx}"), HandleExpandoField));

var children = innerList.Select((i, idx) => ((ExpandoObject)i).Aggregate(new XElement($"{fields.Key}_{idx}"), HandleExpandoField));

Mayble library like https://www.nuget.org/packages/Pluralize.NET.Core/ will do the trick?

GraphQL should also support request with POST method and Content-Type: application/json

Request handling is in FakeServer/GraphQL/GraphQLMiddleware.cs.

Now middleware supports:

If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

Should also support this:

If the "application/json " Content-Type header is present, treat the HTTP POST body contents as a JSON with parameter query having the GraphQL query string.

Method: POST 
Content-Type: application/json 
Body: { "query": "{users{name}}" }

Release unreleased features

Could you release the unreleased features? I'm especially eager to use the "Content negotiation with Accept-header and support for CSV and XML" feature.

Support numeric string for collection identifiers

If a collection has an item with "id": "MTgwODA3MA" and another item with "id": "510". Then GET /api/collection/510 will throw an exception in DynamicController.GetItem on the lamba method ObjectHelper.GetFieldValue(e, _dsSettings.IdField) == id :

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'Operator '==' cannot be applied to operands of type 'string' and 'int''

Do you think it would be interesting to support collections with such numeric string as identifiers?

Thank you very much for this library. It's very interesting, I use it for learning purposes and prototyping.

GraphQL should support requests with GET and POST method and query in the query parameter

Full specification: http://graphql.org/learn/serving-over-http/

Request handling is in FakeServer/GraphQL/GraphQLMiddleware.cs.

Now middleware supports case:

If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

Should also support these:

Method: GET
When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string

/graphql?query={users{name}}

Method: POST
If the "query" query string parameter is present (as in the GET example above), it should be parsed and handled in the same way as the HTTP GET case.

/graphql?query={users{name}}

Broken Auth in Swagger UI

On branch update-to-core-30 the endpoints under Authentification tag in Swagger UI are unusable with various symptoms on the following browsers :

  • On Windows 10 with Chrome
  • On Windows 10 with Firefox
  • On MacOS with Chrome

Those endpoints work fine with cURL commands. The problem comes from Swagger UI.

Note: on branch master with .NET Core 2.2, despite the web browser receives the auth token, the Swagger UI does not display the response. Is it important to fix that, or should we focus our effort on .NET Core 3?

GET /api/{object} support

It works well with collections but it really needs to support JSON objects as endpoint like so:

We have data.json file:

`{
  "my_test_request": {
    "has_access": true,
    "token": "09832145kndse89745",
    "name": "Instagram Honey",
    "data_array": [
     "beer", "disco", "fun"
    ]
  }
}`

GET http://localhost:57602/api/my_test_request

And as response:

 {
    "has_access": true,
    "token": "09832145kndse89745",
    "name": "Instagram Honey",
    "data_array": [
     "beer", "disco", "fun"
    ]
  }

Update to .NET Core 3.1

Migration guide: https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.0&tabs=visual-studio

Branch: https://github.com/ttu/dotnet-fake-json-server/tree/update-to-core-30

New System.Text.Json doesn't support all serializations required by Fake Server, so have to use NewtonsoftJson.

TODO:
[ ] Fix Swagger Authentication filters
[x] Fix InputFormatters / Consumes attribute to accept different media types (merge json)
[x] ETagMiddleware doesn't work with PUT methods. Middleware can't change request's method to GET and verify original data.

Extend support for static files

Current functionality serves files from wwwroot
https://github.com/ttu/dotnet-fake-json-server#static-files

Should work same way as e.g. Node's Serve
https://www.npmjs.com/package/serve

$ npm install -g serve

# Server all files from root
$ serve -s .

Usage could be like this:

$ dotnet install --global FakeServer

# Server files from root
$ fake-server --serve .

# Server files from specific folder
$ fake-server --serve ./files
$ fake-server -s ./files
$ fake-server -s C:\temp\files
$ fake-server -s /home/timmy/app/dist

Reqeust to console and/or file?

Great job on the .NET global tool! Works great!

Is there an option to tell fake server to simply output the request to the console or file?

If not, I'll be happy to try and help. Just point a finger in the best spot to implement the code and I'll make it happen.

Create new appsettings.json from command line

Add init command line parameter that will create default appsettings.json

$ fake-server --init

$ dotnet run --init

This command should be used with global tool as then appsettings.json is not located in the startup directory.

Add Swagger URL to startup output

Current output

Datastore file: datastore.json
Datastore location: /home/tomi/src/github/dotnet-fake-json-server/FakeServer
Static files: default wwwroot
Hosting environment: Development
Content root path: /home/xxxx/dotnet-fake-json-server/FakeServer
Now listening on: http://localhost:57603
Application started. Press Ctrl+C to shut down.

Add Swagger Open API as a last line.

Now listening on: http://localhost:57603
Application started. Press Ctrl+C to shut down.
Swagger Open API is available on: http://localhost:57603/swagger  

Pretty likely this needs to be done in Startup.cs as we need to be able to check what is the current port in use.

Travis CI test (Linux) fail with "System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached ..."

Unit tests on Travis (Linux) fail with

System.IO.IOException : The configured user limit (128) on the number of inotify instances has been reached, or the per-process limit on the number of open file descriptors has been reached.

Example failing test run:
https://app.travis-ci.com/github/ttu/dotnet-fake-json-server/builds/251835568

Example stack trace:

[xUnit.net 00:02:53.56]     FakeServer.Test.Authentication.TokenAuthenticationSpecs.GetToken_InvalidJsonData [FAIL]
  Failed FakeServer.Test.Authentication.TokenAuthenticationSpecs.GetUsers_Authorized_Logout(tokenType: Json) [1 ms]
  Error Message:
   System.IO.IOException : The configured user limit (128) on the number of inotify instances has been reached, or the per-process limit on the number of open file descriptors has been reached.
  Stack Trace:
     at System.IO.FileSystemWatcher.StartRaisingEvents()
   at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.TryEnableFileSystemWatcher()
   at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.CreateFileChangeToken(String filter)
   at Microsoft.Extensions.Primitives.ChangeToken.OnChange(Func`1 changeTokenProducer, Action changeTokenConsumer)
   at Microsoft.Extensions.Configuration.FileConfigurationProvider..ctor(FileConfigurationSource source)
   at Microsoft.Extensions.Configuration.Json.JsonConfigurationSource.Build(IConfigurationBuilder builder)
   at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
   at Microsoft.AspNetCore.TestHost.TestServer..ctor(IWebHostBuilder builder, IFeatureCollection featureCollection)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateServer(IWebHostBuilder builder)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.EnsureServer()
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateDefaultClient(DelegatingHandler[] handlers)
   at Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`1.CreateClient(WebApplicationFactoryClientOptions options)
   at FakeServer.Test.IntegrationFixture.StartServer(String authenticationType) in /home/travis/build/ttu/dotnet-fake-json-server/FakeServer.Test/IntegrationTestCollection.cs:line 69
   at FakeServer.Test.Authentication.TokenAuthenticationSpecs..ctor(IntegrationFixture fixture) in /home/travis/build/ttu/dotnet-fake-json-server/FakeServer.Test/Authentication/TokenAuthenticationSpecs.cs:line 24

DELETE operations put server in Bad State

DELETE /api/{collection}/{id}
succeeds only once. If there are other collections, server will return 500.

Sample data.json:

{
  "tests": [
    {
      "value": [
        "Test1"
      ],
      "id": 0
    }
  ],
  "items": [
    {
      "description": "some_description",
      "createdDate": "0001-01-01T00:00:00",
      "id": 0
    }
  ],
  "programs": [
    {
      "id": 0,
      "canOverride": false,
      "enabled": true,
      "version": null
    }
  ]
}

Operation 1
DELETE /api/tests/0
result: 204

Operation 2
DELETE /api/items/0
result: 500

Controller Path/Tree Support

using a web service tree with controller paths like:

garage/v3.0/storage/Bins
garage/v3.0/storage/Shelves
garage/v3.0/workbench/Tools

I haven't been able to figure out how to mock these, even manually editing datastore.json as follows and calling Reload:

{ "garage/v3.0/storage/Bins": [ { "Type": "Large", "id": 0 }, { "Type": "Small", "id": 1 } ] }

I see there is support for nested items, but this requires specifying an item ID at each level which will not match the paths used by the real service.

Upgrade McMaster.Extensions.CommandLineUtils to version 4.x

Error when upgrading from current version 3.1.0 to 4.0.2

FakeServer
  Program.cs(136, 44): [CS0411] The type arguments for method 'ConfigurationExtensions.Add<TSource>(IConfigurationBuilder, Action<TSource>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
  Program.cs(137, 44): [CS0411] The type arguments for method 'ConfigurationExtensions.Add<TSource>(IConfigurationBuilder, Action<TSource>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
  Program.cs(156, 54): [CS1061] 'IReadOnlyList<string>' does not contain a definition for 'ToArray' and no accessible extension method 'ToArray' accepting a first argument of type 'IReadOnlyList<string>' could be found (are you missing a using directive or an assembly reference?)

https://github.com/ttu/dotnet-fake-json-server/blob/master/FakeServer/Program.cs#L136

Find the correct way to add arguments. Maybe will work with app.Arguments.Add ?

Merge patch should support nested properties

Should be able to update e.g. only parent with id 1

PATCH /api/families/19/parents/1

{
  "name": "Kyle",
  "work": {
    "companyName": "PAWNAGRA",
    "address": "679 Ebony Court, Loma, Puerto Rico, 7716"
  }
}

Current implementation supports patch only from the root-level e.g. /api/families/19.

Using Id located somewhere else

Hi,

Is there a way to reference an idea inside a collection rather than a straight forward property?
Using your "users" as an example:

"users": [
    {
      "id": 1,
      "name": "James",
      "age": 40,
      "location": "NY",
      "work": {
        "name": "ACME",
        "location": "NY",
        "rating": 2.3
      }
]

To access it I can user users/1 but what if my Id is inside one of the propeties? for eg:

"users": [
    {
      "name": "James",
      "age": 40,
      "location": "NY",
      "work": {
        "id": 1,
        "name": "ACME",
        "location": "NY",
        "rating": 2.3
      }
    }
]

Is there a way to access this record with "users/{id} but when it gets executed it points to "users/work/{id}" ?

Thanks

Bad XML tags occuring when getting single family

When using the command "curl -H "Accept: text/xml" http://localhost:57602/api/families/1", it appears that an errant <friends />" tag (not </friends>) appears right before a </child> tag.

      </friends>
    </child>
    <child>
      <name>Katheryn</name>
      <gender>female</gender>
      <age>9</age>
      <friends />
    </child>
  </children>

For the command "curl -H "Accept: text/xml" http://localhost:57602/api/families/0", an errant <children /> tag occurs after </parents>

    </parent>
  </parents>
  <children />
  <address>
    <postNumber>147</postNumber>
    <street>Melba Court</street>

I did some testing and was able to replicate this problem before the changes in XML pluralization, but I wasn't able to find an immediate cause.

Specifying a data file with the "--file" argument causes a duplicate key exception

Steps to reproduce:

  1. Clone the repo
  2. cd into the dotnet-fake-json-server/FakeServer directory
  3. Create a file called "test.json"
  4. Run the application with the following command: dotnet run --file "test.json"

I received the following trace after dotnet crashed:

PS C:\Users\aschuhardt\dotnet-fake-json-server\FakeServer> dotnet run --file "test.json"
Using launch settings from C:\Users\aschuhardt\dotnet-fake-json-server\FakeServer\Properties\launchSettings.json...
File: test.json

Unhandled Exception: System.ArgumentException: An item with the same key has already been added. Key: file
   at System.ThrowHelper.ThrowAddingDuplicateWithKeyArgumentException(Object key)
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
   at FakeServer.Program.Main(String[] args) in C:\Users\aschuhardt\dotnet-fake-json-server\FakeServer\Program.cs:line 38

Cause:

This line adds a key "file" regardless of whether one has been added as a result of the command-line argument

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.